* (bug 5445) Now remove autoblocks when a user is unblocked.
* Added $wgLogExceptionBacktrace, on by default, to allow logging of exception
backtraces.
-* Added device detection for determining device capabilities
+* Added device detection for determining device capabilities.
+* QUnit.newMwEnvironment now supports passing a custom setup and/or teardown function.
+ Arguments signature has changed. First arguments is now an options object of which
+ 'config' can be a property. Previously 'config' itself was the first and only argument.
=== Bug fixes in 1.20 ===
* (bug 30245) Use the correct way to construct a log page title.
'FileBackendStoreShardDirIterator' => 'includes/filerepo/backend/FileBackendStore.php',
'FileBackendStoreShardFileIterator' => 'includes/filerepo/backend/FileBackendStore.php',
'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php',
+ 'FileBackendStoreOpHandle' => 'includes/filerepo/backend/FileBackendStore.php',
'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php',
'FSFileBackendList' => 'includes/filerepo/backend/FSFileBackend.php',
'FSFileBackendDirList' => 'includes/filerepo/backend/FSFileBackend.php',
'FSFileBackendFileList' => 'includes/filerepo/backend/FSFileBackend.php',
+ 'FSFileOpHandle' => 'includes/filerepo/backend/FSFileBackend.php',
'SwiftFileBackend' => 'includes/filerepo/backend/SwiftFileBackend.php',
'SwiftFileBackendList' => 'includes/filerepo/backend/SwiftFileBackend.php',
'SwiftFileBackendDirList' => 'includes/filerepo/backend/SwiftFileBackend.php',
'SwiftFileBackendFileList' => 'includes/filerepo/backend/SwiftFileBackend.php',
+ 'SwiftFileOpHandle' => 'includes/filerepo/backend/SwiftFileBackend.php',
'FileJournal' => 'includes/filerepo/backend/filejournal/FileJournal.php',
'DBFileJournal' => 'includes/filerepo/backend/filejournal/DBFileJournal.php',
'NullFileJournal' => 'includes/filerepo/backend/filejournal/FileJournal.php',
'MySqlLockManager'=> 'includes/filerepo/backend/lockmanager/DBLockManager.php',
'NullLockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php',
'FileOp' => 'includes/filerepo/backend/FileOp.php',
+ 'FileOpBatch' => 'includes/filerepo/backend/FileOpBatch.php',
'StoreFileOp' => 'includes/filerepo/backend/FileOp.php',
'CopyFileOp' => 'includes/filerepo/backend/FileOp.php',
'MoveFileOp' => 'includes/filerepo/backend/FileOp.php',
}
}
- $ok = copy( $params['src'], $dest );
- // In some cases (at least over NFS), copy() returns true when it fails.
- if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
- if ( $ok ) { // PHP bug
- unlink( $dest ); // remove broken file
- trigger_error( __METHOD__ . ": copy() failed but returned true." );
+ if ( !empty( $params['async'] ) ) { // deferred
+ $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp',
+ wfEscapeShellArg( $this->cleanPathSlashes( $params['src'] ) ),
+ wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
+ ) );
+ $status->value = new FSFileOpHandle( $this, $params, 'Store', $cmd, $dest );
+ } else { // immediate write
+ $ok = copy( $params['src'], $dest );
+ // In some cases (at least over NFS), copy() returns true when it fails
+ if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
+ if ( $ok ) { // PHP bug
+ unlink( $dest ); // remove broken file
+ trigger_error( __METHOD__ . ": copy() failed but returned true." );
+ }
+ $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
+ return $status;
}
- $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
- return $status;
+ $this->chmod( $dest );
}
- $this->chmod( $dest );
-
return $status;
}
+ /**
+ * @see FSFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseStore( $errors, Status $status, array $params, $cmd ) {
+ if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
+ $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
+ trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+ }
+ }
+
/**
* @see FileBackendStore::doCopyInternal()
* @return Status
}
}
- $ok = copy( $source, $dest );
- // In some cases (at least over NFS), copy() returns true when it fails.
- if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
- if ( $ok ) { // PHP bug
- unlink( $dest ); // remove broken file
- trigger_error( __METHOD__ . ": copy() failed but returned true." );
+ if ( !empty( $params['async'] ) ) { // deferred
+ $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp',
+ wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
+ wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
+ ) );
+ $status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd, $dest );
+ } else { // immediate write
+ $ok = copy( $source, $dest );
+ // In some cases (at least over NFS), copy() returns true when it fails
+ if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
+ if ( $ok ) { // PHP bug
+ unlink( $dest ); // remove broken file
+ trigger_error( __METHOD__ . ": copy() failed but returned true." );
+ }
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ return $status;
}
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
- return $status;
+ $this->chmod( $dest );
}
- $this->chmod( $dest );
-
return $status;
}
+ /**
+ * @see FSFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseCopy( $errors, Status $status, array $params, $cmd ) {
+ if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+ }
+ }
+
/**
* @see FileBackendStore::doMoveInternal()
* @return Status
}
}
- $ok = rename( $source, $dest );
- clearstatcache(); // file no longer at source
- if ( !$ok ) {
- $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
- return $status;
+ if ( !empty( $params['async'] ) ) { // deferred
+ $cmd = implode( ' ', array( wfIsWindows() ? 'MOVE' : 'mv',
+ wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
+ wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
+ ) );
+ $status->value = new FSFileOpHandle( $this, $params, 'Move', $cmd );
+ } else { // immediate write
+ $ok = rename( $source, $dest );
+ clearstatcache(); // file no longer at source
+ if ( !$ok ) {
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ return $status;
+ }
}
return $status;
}
+ /**
+ * @see FSFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseMove( $errors, Status $status, array $params, $cmd ) {
+ if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+ }
+ }
+
/**
* @see FileBackendStore::doDeleteInternal()
* @return Status
return $status; // do nothing; either OK or bad status
}
- $ok = unlink( $source );
- if ( !$ok ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
- return $status;
+ if ( !empty( $params['async'] ) ) { // deferred
+ $cmd = implode( ' ', array( wfIsWindows() ? 'DEL' : 'unlink',
+ wfEscapeShellArg( $this->cleanPathSlashes( $source ) )
+ ) );
+ $status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd );
+ } else { // immediate write
+ $ok = unlink( $source );
+ if ( !$ok ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ return $status;
+ }
}
return $status;
}
+ /**
+ * @see FSFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseDelete( $errors, Status $status, array $params, $cmd ) {
+ if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+ }
+ }
+
/**
* @see FileBackendStore::doCreateInternal()
* @return Status
}
}
- $bytes = file_put_contents( $dest, $params['content'] );
- if ( $bytes === false ) {
- $status->fatal( 'backend-fail-create', $params['dst'] );
- return $status;
+ if ( !empty( $params['async'] ) ) { // deferred
+ $tempFile = TempFSFile::factory( 'create_', 'tmp' );
+ if ( !$tempFile ) {
+ $status->fatal( 'backend-fail-create', $params['dst'] );
+ return $status;
+ }
+ $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
+ if ( $bytes === false ) {
+ $status->fatal( 'backend-fail-create', $params['dst'] );
+ return $status;
+ }
+ $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp',
+ wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
+ wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
+ ) );
+ $status->value = new FSFileOpHandle( $this, $params, 'Create', $cmd, $dest );
+ $tempFile->bind( $status->value );
+ } else { // immediate write
+ $bytes = file_put_contents( $dest, $params['content'] );
+ if ( $bytes === false ) {
+ $status->fatal( 'backend-fail-create', $params['dst'] );
+ return $status;
+ }
+ $this->chmod( $dest );
}
- $this->chmod( $dest );
-
return $status;
}
+ /**
+ * @see FSFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseCreate( $errors, Status $status, array $params, $cmd ) {
+ if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
+ $status->fatal( 'backend-fail-create', $params['dst'] );
+ trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+ }
+ }
+
/**
* @see FileBackendStore::doPrepareInternal()
* @return Status
return false;
}
+ /**
+ * @see FileBackendStore::doExecuteOpHandlesInternal()
+ * @return Array List of corresponding Status objects
+ */
+ protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
+ $statuses = array();
+
+ $pipes = array();
+ foreach ( $fileOpHandles as $index => $fileOpHandle ) {
+ $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' );
+ }
+
+ $errs = array();
+ foreach ( $pipes as $index => $pipe ) {
+ // Result will be empty on success in *NIX. On Windows,
+ // it may be something like " 1 file(s) [copied|moved].".
+ $errs[$index] = stream_get_contents( $pipe );
+ fclose( $pipe );
+ }
+
+ foreach ( $fileOpHandles as $index => $fileOpHandle ) {
+ $status = Status::newGood();
+ $function = '_getResponse' . $fileOpHandle->call;
+ $this->$function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
+ $statuses[$index] = $status;
+ if ( $status->isOK() && $fileOpHandle->chmodPath ) {
+ $this->chmod( $fileOpHandle->chmodPath );
+ }
+ }
+
+ clearstatcache(); // files changed
+ return $statuses;
+ }
+
/**
* Chmod a file, suppressing the warnings
*
return $ok;
}
+ /**
+ * Clean up directory separators for the given OS
+ *
+ * @param $path string FS path
+ * @return string
+ */
+ protected function cleanPathSlashes( $path ) {
+ return wfIsWindows() ? strtr( $path, '/', '\\' ) : $path;
+ }
+
/**
* Listen for E_WARNING errors and track whether any happen
*
}
}
+/**
+ * @see FileBackendStoreOpHandle
+ */
+class FSFileOpHandle extends FileBackendStoreOpHandle {
+ public $cmd; // string; shell command
+ public $chmodPath; // string; file to chmod
+
+ public function __construct( $backend, array $params, $call, $cmd, $chmodPath = null ) {
+ $this->backend = $backend;
+ $this->params = $params;
+ $this->call = $call;
+ $this->cmd = $cmd;
+ $this->chmodPath = $chmodPath;
+ }
+}
+
/**
* Wrapper around RecursiveDirectoryIterator/DirectoryIterator that
* catches exception or does any custom behavoir that we may want.
protected $name; // string; unique backend name
protected $wikiId; // string; unique wiki name
protected $readOnly; // string; read-only explanation message
+ protected $parallelize; // string; when to do operations in parallel
+ protected $concurrency; // integer; how many operations can be done in parallel
+
/** @var LockManager */
protected $lockManager;
/** @var FileJournal */
* Journals simply log changes to files stored in the backend.
* 'readOnly' : Write operations are disallowed if this is a non-empty string.
* It should be an explanation for the backend being read-only.
+ * 'parallelize' : When to do file operations in parallel (when possible).
+ * Allowed values are "implicit", "explicit" and "off".
+ * 'concurrency' : How many file operations can be done in parallel.
*
* @param $config Array
*/
$this->readOnly = isset( $config['readOnly'] )
? (string)$config['readOnly']
: '';
+ $this->parallelize = isset( $config['parallelize'] )
+ ? (string)$config['parallelize']
+ : 'off';
+ $this->concurrency = isset( $config['concurrency'] )
+ ? (int)$config['concurrency']
+ : 50;
}
/**
* This has no effect unless the 'force' flag is set.
* 'nonJournaled' : Don't log this operation batch in the file journal.
* This limits the ability of recovery scripts.
+ * 'parallelize' : Try to do operations in parallel when possible.
*
* Remarks on locking:
* File system paths given to operations should refer to files that are
unset( $opts['nonLocking'] );
unset( $opts['allowStale'] );
}
+ $opts['concurrency'] = 1; // off
+ if ( $this->parallelize === 'implicit' ) {
+ if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
+ $opts['concurrency'] = $this->concurrency;
+ }
+ } elseif ( $this->parallelize === 'explicit' ) {
+ if ( !empty( $opts['parallelize'] ) ) {
+ $opts['concurrency'] = $this->concurrency;
+ }
+ }
return $this->doOperationsInternal( $ops, $opts );
}
}
// Actually attempt the operation batch...
- $subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
+ $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
$success = array();
$failCount = 0;
* content : the raw file contents
* dst : destination storage path
* overwrite : overwrite any file that exists at the destination
+ * async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
*
* @param $params Array
* @return Status
* src : source path on disk
* dst : destination storage path
* overwrite : overwrite any file that exists at the destination
+ * async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
*
* @param $params Array
* @return Status
* src : source storage path
* dst : destination storage path
* overwrite : overwrite any file that exists at the destination
+ * async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
*
* @param $params Array
* @return Status
* $params include:
* src : source storage path
* ignoreMissingSource : do nothing if the source file does not exist
+ * async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
*
* @param $params Array
* @return Status
* src : source storage path
* dst : destination storage path
* overwrite : overwrite any file that exists at the destination
+ * async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
*
* @param $params Array
* @return Status
* @return Status
*/
protected function doMoveInternal( array $params ) {
+ unset( $params['async'] ); // two steps, won't work here :)
// Copy source to dest
$status = $this->copyInternal( $params );
if ( $status->isOK() ) {
$this->primeContainerCache( $performOps );
// Actually attempt the operation batch...
- $subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
+ $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
// Merge errors into status fields
$status->merge( $subStatus );
return $status;
}
+ /**
+ * Execute a list of FileBackendStoreOpHandle handles in parallel.
+ * The resulting Status object fields will correspond
+ * to the order in which the handles where given.
+ *
+ * @param $handles Array List of FileBackendStoreOpHandle objects
+ * @return Array Map of Status objects
+ */
+ final public function executeOpHandlesInternal( array $fileOpHandles ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-' . $this->name );
+ foreach ( $fileOpHandles as $fileOpHandle ) {
+ if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
+ throw new MWException( "Given a non-FileBackendStoreOpHandle object." );
+ } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
+ throw new MWException( "Given a FileBackendStoreOpHandle for the wrong backend." );
+ }
+ }
+ $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $res;
+ }
+
+ /**
+ * @see FileBackendStore::executeOpHandlesInternal()
+ * @return Array List of corresponding Status objects
+ */
+ protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
+ foreach ( $fileOpHandles as $fileOpHandle ) { // OK if empty
+ throw new MWException( "This backend supports no asynchronous operations." );
+ }
+ return array();
+ }
+
/**
* @see FileBackend::clearCache()
*/
final protected function primeContainerCache( array $items ) {
wfProfileIn( __METHOD__ );
wfProfileIn( __METHOD__ . '-' . $this->name );
+
$paths = array(); // list of storage paths
$contNames = array(); // (cache key => resolved container name)
// Get all the paths/containers from the items...
// Populate the container process cache for the backend...
$this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
+
wfProfileOut( __METHOD__ . '-' . $this->name );
wfProfileOut( __METHOD__ );
}
final protected function primeFileCache( array $items ) {
wfProfileIn( __METHOD__ );
wfProfileIn( __METHOD__ . '-' . $this->name );
+
$paths = array(); // list of storage paths
$pathNames = array(); // (cache key => storage path)
// Get all the paths/containers from the items...
$this->cache[$pathNames[$cacheKey]]['stat'] = $val;
}
}
+
wfProfileOut( __METHOD__ . '-' . $this->name );
wfProfileOut( __METHOD__ );
}
}
+/**
+ * FileBackendStore helper class for performing asynchronous file operations.
+ *
+ * For example, calling FileBackendStore::createInternal() with the "async"
+ * param flag may result in a Status that contains this object as a value.
+ * This class is largely backend-specific and is mostly just "magic" to be
+ * passed to FileBackendStore::executeOpHandlesInternal().
+ */
+abstract class FileBackendStoreOpHandle {
+ /** @var Array */
+ public $params = array(); // params to caller functions
+ /** @var FileBackendStore */
+ public $backend;
+ /** @var Array */
+ public $resourcesToClose = array();
+
+ public $call; // string; name that identifies the function called
+
+ /**
+ * Close all open file handles
+ *
+ * @return void
+ */
+ public function closeResources() {
+ array_map( 'fclose', $this->resourcesToClose );
+ }
+}
+
/**
* FileBackendStore helper function to handle listings that span container shards.
* Do not use this class from places outside of FileBackendStore.
*/
/**
- * Helper class for representing operations with transaction support.
+ * FileBackend helper class for representing operations.
* Do not use this class from places outside FileBackend.
*
- * Methods called from attemptBatch() should avoid throwing exceptions at all costs.
- * FileOp objects should be lightweight in order to support large arrays in memory.
+ * Methods called from FileOpBatch::attempt() should avoid throwing
+ * exceptions at all costs. FileOp objects should be lightweight in order
+ * to support large arrays in memory and serialization.
*
* @ingroup FileBackend
* @since 1.19
protected $state = self::STATE_NEW; // integer
protected $failed = false; // boolean
+ protected $async = false; // boolean
protected $useLatest = true; // boolean
protected $batchId; // string
const STATE_CHECKED = 2;
const STATE_ATTEMPTED = 3;
- /* Timeout related parameters */
- const MAX_BATCH_SIZE = 1000;
- const TIME_LIMIT_SEC = 300; // 5 minutes
-
/**
* Build a new file operation transaction
*
* @param $batchId string
* @return void
*/
- final protected function setBatchId( $batchId ) {
+ final public function setBatchId( $batchId ) {
$this->batchId = $batchId;
}
* @param $allowStale bool
* @return void
*/
- final protected function allowStaleReads( $allowStale ) {
+ final public function allowStaleReads( $allowStale ) {
$this->useLatest = !$allowStale;
}
/**
- * Attempt to perform a series of file operations.
- * Callers are responsible for handling file locking.
+ * Get the value of the parameter with the given name
*
- * $opts is an array of options, including:
- * 'force' : Errors that would normally cause a rollback do not.
- * The remaining operations are still attempted if any fail.
- * 'allowStale' : Don't require the latest available data.
- * This can increase performance for non-critical writes.
- * This has no effect unless the 'force' flag is set.
- * 'nonJournaled' : Don't log this operation batch in the file journal.
+ * @param $name string
+ * @return mixed Returns null if the parameter is not set
+ */
+ final public function getParam( $name ) {
+ return isset( $this->params[$name] ) ? $this->params[$name] : null;
+ }
+
+ /**
+ * Check if this operation failed precheck() or attempt()
*
- * The resulting Status will be "OK" unless:
- * a) unexpected operation errors occurred (network partitions, disk full...)
- * b) significant operation errors occured and 'force' was not set
+ * @return bool
+ */
+ final public function failed() {
+ return $this->failed;
+ }
+
+ /**
+ * Get a new empty predicates array for precheck()
*
- * @param $performOps Array List of FileOp operations
- * @param $opts Array Batch operation options
- * @param $journal FileJournal Journal to log operations to
- * @return Status
+ * @return Array
*/
- final public static function attemptBatch(
- array $performOps, array $opts, FileJournal $journal
- ) {
- $status = Status::newGood();
+ final public static function newPredicates() {
+ return array( 'exists' => array(), 'sha1' => array() );
+ }
- $n = count( $performOps );
- if ( $n > self::MAX_BATCH_SIZE ) {
- $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE );
- return $status;
- }
+ /**
+ * Get a new empty dependency tracking array for paths read/written to
+ *
+ * @return Array
+ */
+ final public static function newDependencies() {
+ return array( 'read' => array(), 'write' => array() );
+ }
- $batchId = $journal->getTimestampedUUID();
- $allowStale = !empty( $opts['allowStale'] );
- $ignoreErrors = !empty( $opts['force'] );
- $journaled = empty( $opts['nonJournaled'] );
-
- $entries = array(); // file journal entries
- $predicates = FileOp::newPredicates(); // account for previous op in prechecks
- // Do pre-checks for each operation; abort on failure...
- foreach ( $performOps as $index => $fileOp ) {
- $fileOp->setBatchId( $batchId );
- $fileOp->allowStaleReads( $allowStale );
- $oldPredicates = $predicates;
- $subStatus = $fileOp->precheck( $predicates ); // updates $predicates
- $status->merge( $subStatus );
- if ( $subStatus->isOK() ) {
- if ( $journaled ) { // journal log entry
- $entries = array_merge( $entries,
- self::getJournalEntries( $fileOp, $oldPredicates, $predicates ) );
- }
- } else { // operation failed?
- $status->success[$index] = false;
- ++$status->failCount;
- if ( !$ignoreErrors ) {
- return $status; // abort
- }
- }
- }
+ /**
+ * Update a dependency tracking array to account for this operation
+ *
+ * @param $deps Array Prior path reads/writes; format of FileOp::newPredicates()
+ * @return Array
+ */
+ final public function applyDependencies( array $deps ) {
+ $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
+ $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
+ return $deps;
+ }
- // Log the operations in file journal...
- if ( count( $entries ) ) {
- $subStatus = $journal->logChangeBatch( $entries, $batchId );
- if ( !$subStatus->isOK() ) {
- return $subStatus; // abort
+ /**
+ * Check if this operation changes files listed in $paths
+ *
+ * @param $paths Array Prior path reads/writes; format of FileOp::newPredicates()
+ * @return boolean
+ */
+ final public function dependsOn( array $deps ) {
+ foreach ( $this->storagePathsChanged() as $path ) {
+ if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
+ return true; // "output" or "anti" dependency
}
}
-
- if ( $ignoreErrors ) { // treat precheck() fatals as mere warnings
- $status->setResult( true, $status->value );
- }
-
- // Attempt each operation...
- foreach ( $performOps as $index => $fileOp ) {
- if ( $fileOp->failed() ) {
- continue; // nothing to do
- }
- $subStatus = $fileOp->attempt();
- $status->merge( $subStatus );
- if ( $subStatus->isOK() ) {
- $status->success[$index] = true;
- ++$status->successCount;
- } else {
- $status->success[$index] = false;
- ++$status->failCount;
- // We can't continue (even with $ignoreErrors) as $predicates is wrong.
- // Log the remaining ops as failed for recovery...
- for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) {
- $performOps[$i]->logFailure( 'attempt_aborted' );
- }
- return $status; // bail out
+ foreach ( $this->storagePathsRead() as $path ) {
+ if ( isset( $deps['write'][$path] ) ) {
+ return true; // "flow" dependency
}
}
-
- return $status;
+ return false;
}
/**
- * Get the file journal entries for a single file operation
+ * Get the file journal entries for this file operation
*
- * @param $fileOp FileOp
- * @param $oPredicates Array Pre-op information about files
- * @param $nPredicates Array Post-op information about files
+ * @param $oPredicates Array Pre-op info about files (format of FileOp::newPredicates)
+ * @param $nPredicates Array Post-op info about files (format of FileOp::newPredicates)
* @return Array
*/
- final protected static function getJournalEntries(
- FileOp $fileOp, array $oPredicates, array $nPredicates
- ) {
+ final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
$nullEntries = array();
$updateEntries = array();
$deleteEntries = array();
- $pathsUsed = array_merge( $fileOp->storagePathsRead(), $fileOp->storagePathsChanged() );
+ $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
foreach ( $pathsUsed as $path ) {
$nullEntries[] = array( // assertion for recovery
'op' => 'null',
'path' => $path,
- 'newSha1' => $fileOp->fileSha1( $path, $oPredicates )
+ 'newSha1' => $this->fileSha1( $path, $oPredicates )
);
}
- foreach ( $fileOp->storagePathsChanged() as $path ) {
+ foreach ( $this->storagePathsChanged() as $path ) {
if ( $nPredicates['sha1'][$path] === false ) { // deleted
$deleteEntries[] = array(
'op' => 'delete',
);
} else { // created/updated
$updateEntries[] = array(
- 'op' => $fileOp->fileExists( $path, $oPredicates ) ? 'update' : 'create',
+ 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
'path' => $path,
'newSha1' => $nPredicates['sha1'][$path]
);
return array_merge( $nullEntries, $updateEntries, $deleteEntries );
}
- /**
- * Get the value of the parameter with the given name
- *
- * @param $name string
- * @return mixed Returns null if the parameter is not set
- */
- final public function getParam( $name ) {
- return isset( $this->params[$name] ) ? $this->params[$name] : null;
- }
-
- /**
- * Check if this operation failed precheck() or attempt()
- *
- * @return bool
- */
- final public function failed() {
- return $this->failed;
- }
-
- /**
- * Get a new empty predicates array for precheck()
- *
- * @return Array
- */
- final public static function newPredicates() {
- return array( 'exists' => array(), 'sha1' => array() );
- }
-
/**
* Check preconditions of the operation without writing anything
*
}
/**
- * Attempt the operation, backing up files as needed; this must be reversible
+ * @return Status
+ */
+ protected function doPrecheck( array &$predicates ) {
+ return Status::newGood();
+ }
+
+ /**
+ * Attempt the operation
*
* @return Status
*/
return $status;
}
+ /**
+ * @return Status
+ */
+ protected function doAttempt() {
+ return Status::newGood();
+ }
+
+ /**
+ * Attempt the operation in the background
+ *
+ * @return Status
+ */
+ final public function attemptAsync() {
+ $this->async = true;
+ $result = $this->attempt();
+ $this->async = false;
+ return $result;
+ }
+
/**
* Get the file operation parameters
*
return array( array(), array() );
}
+ /**
+ * Adjust params to FileBackendStore internal file calls
+ *
+ * @param $params Array
+ * @return Array (required params list, optional params list)
+ */
+ protected function setFlags( array $params ) {
+ return array( 'async' => $this->async ) + $params;
+ }
+
/**
* Get a list of storage paths read from for this operation
*
* @return Array
*/
- public function storagePathsRead() {
- return array();
+ final public function storagePathsRead() {
+ return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsRead() );
}
/**
- * Get a list of storage paths written to for this operation
- *
+ * @see FileOp::storagePathsRead()
* @return Array
*/
- public function storagePathsChanged() {
+ protected function doStoragePathsRead() {
return array();
}
/**
- * @return Status
+ * Get a list of storage paths written to for this operation
+ *
+ * @return Array
*/
- protected function doPrecheck( array &$predicates ) {
- return Status::newGood();
+ final public function storagePathsChanged() {
+ return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsChanged() );
}
/**
- * @return Status
+ * @see FileOp::storagePathsChanged()
+ * @return Array
*/
- protected function doAttempt() {
- return Status::newGood();
+ protected function doStoragePathsChanged() {
+ return array();
}
/**
}
}
+ /**
+ * Get the backend this operation is for
+ *
+ * @return FileBackendStore
+ */
+ public function getBackend() {
+ return $this->backend;
+ }
+
/**
* Log a file operation failure and preserve any temp files
*
* @param $action string
* @return void
*/
- final protected function logFailure( $action ) {
+ final public function logFailure( $action ) {
$params = $this->params;
$params['failedAction'] = $action;
try {
}
protected function doAttempt() {
- $status = Status::newGood();
// Store the file at the destination
if ( !$this->destSameAsSource ) {
- $status->merge( $this->backend->storeInternal( $this->params ) );
+ return $this->backend->storeInternal( $this->setFlags( $this->params ) );
}
- return $status;
+ return Status::newGood();
}
protected function getSourceSha1Base36() {
return $hash;
}
- public function storagePathsChanged() {
+ protected function doStoragePathsChanged() {
return array( $this->params['dst'] );
}
}
}
protected function doAttempt() {
- $status = Status::newGood();
- // Create the file at the destination
if ( !$this->destSameAsSource ) {
- $status->merge( $this->backend->createInternal( $this->params ) );
+ // Create the file at the destination
+ return $this->backend->createInternal( $this->setFlags( $this->params ) );
}
- return $status;
+ return Status::newGood();
}
protected function getSourceSha1Base36() {
return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
}
- public function storagePathsChanged() {
+ protected function doStoragePathsChanged() {
return array( $this->params['dst'] );
}
}
}
protected function doAttempt() {
- $status = Status::newGood();
// Do nothing if the src/dst paths are the same
if ( $this->params['src'] !== $this->params['dst'] ) {
// Copy the file into the destination
if ( !$this->destSameAsSource ) {
- $status->merge( $this->backend->copyInternal( $this->params ) );
+ return $this->backend->copyInternal( $this->setFlags( $this->params ) );
}
}
- return $status;
+ return Status::newGood();
}
- public function storagePathsRead() {
+ protected function doStoragePathsRead() {
return array( $this->params['src'] );
}
- public function storagePathsChanged() {
+ protected function doStoragePathsChanged() {
return array( $this->params['dst'] );
}
}
}
protected function doAttempt() {
- $status = Status::newGood();
// Do nothing if the src/dst paths are the same
if ( $this->params['src'] !== $this->params['dst'] ) {
if ( !$this->destSameAsSource ) {
// Move the file into the destination
- $status->merge( $this->backend->moveInternal( $this->params ) );
+ return $this->backend->moveInternal( $this->setFlags( $this->params ) );
} else {
// Just delete source as the destination needs no changes
$params = array( 'src' => $this->params['src'] );
- $status->merge( $this->backend->deleteInternal( $params ) );
+ return $this->backend->deleteInternal( $this->setFlags( $params ) );
}
}
- return $status;
+ return Status::newGood();
}
- public function storagePathsRead() {
+ protected function doStoragePathsRead() {
return array( $this->params['src'] );
}
- public function storagePathsChanged() {
+ protected function doStoragePathsChanged() {
return array( $this->params['src'], $this->params['dst'] );
}
}
}
protected function doAttempt() {
- $status = Status::newGood();
if ( $this->needsDelete ) {
// Delete the source file
- $status->merge( $this->backend->deleteInternal( $this->params ) );
+ return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
}
- return $status;
+ return Status::newGood();
}
- public function storagePathsChanged() {
+ protected function doStoragePathsChanged() {
return array( $this->params['src'] );
}
}
--- /dev/null
+<?php
+/**
+ * @file
+ * @ingroup FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * Helper class for representing batch file operations.
+ * Do not use this class from places outside FileBackend.
+ *
+ * Methods should avoid throwing exceptions at all costs.
+ *
+ * @ingroup FileBackend
+ * @since 1.20
+ */
+class FileOpBatch {
+ /* Timeout related parameters */
+ const MAX_BATCH_SIZE = 1000; // integer
+
+ /**
+ * Attempt to perform a series of file operations.
+ * Callers are responsible for handling file locking.
+ *
+ * $opts is an array of options, including:
+ * 'force' : Errors that would normally cause a rollback do not.
+ * The remaining operations are still attempted if any fail.
+ * 'allowStale' : Don't require the latest available data.
+ * This can increase performance for non-critical writes.
+ * This has no effect unless the 'force' flag is set.
+ * 'nonJournaled' : Don't log this operation batch in the file journal.
+ * 'concurrency' : Try to do this many operations in parallel when possible.
+ *
+ * The resulting Status will be "OK" unless:
+ * a) unexpected operation errors occurred (network partitions, disk full...)
+ * b) significant operation errors occured and 'force' was not set
+ *
+ * @param $performOps Array List of FileOp operations
+ * @param $opts Array Batch operation options
+ * @param $journal FileJournal Journal to log operations to
+ * @return Status
+ */
+ public static function attempt( array $performOps, array $opts, FileJournal $journal ) {
+ wfProfileIn( __METHOD__ );
+ $status = Status::newGood();
+
+ $n = count( $performOps );
+ if ( $n > self::MAX_BATCH_SIZE ) {
+ $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ $batchId = $journal->getTimestampedUUID();
+ $allowStale = !empty( $opts['allowStale'] );
+ $ignoreErrors = !empty( $opts['force'] );
+ $journaled = empty( $opts['nonJournaled'] );
+ $maxConcurrency = isset( $opts['concurrency'] ) ? $opts['concurrency'] : 1;
+
+ $entries = array(); // file journal entry list
+ $predicates = FileOp::newPredicates(); // account for previous ops in prechecks
+ $curBatch = array(); // concurrent FileOp sub-batch accumulation
+ $curBatchDeps = FileOp::newDependencies(); // paths used in FileOp sub-batch
+ $pPerformOps = array(); // ordered list of concurrent FileOp sub-batches
+ $lastBackend = null; // last op backend name
+ // Do pre-checks for each operation; abort on failure...
+ foreach ( $performOps as $index => $fileOp ) {
+ $backendName = $fileOp->getBackend()->getName();
+ $fileOp->setBatchId( $batchId ); // transaction ID
+ $fileOp->allowStaleReads( $allowStale ); // consistency level
+ // Decide if this op can be done concurrently within this sub-batch
+ // or if a new concurrent sub-batch must be started after this one...
+ if ( $fileOp->dependsOn( $curBatchDeps )
+ || count( $curBatch ) >= $maxConcurrency
+ || ( $backendName !== $lastBackend && count( $curBatch ) )
+ ) {
+ $pPerformOps[] = $curBatch; // push this batch
+ $curBatch = array(); // start a new sub-batch
+ $curBatchDeps = FileOp::newDependencies();
+ }
+ $lastBackend = $backendName;
+ $curBatch[$index] = $fileOp; // keep index
+ // Update list of affected paths in this batch
+ $curBatchDeps = $fileOp->applyDependencies( $curBatchDeps );
+ // Simulate performing the operation...
+ $oldPredicates = $predicates;
+ $subStatus = $fileOp->precheck( $predicates ); // updates $predicates
+ $status->merge( $subStatus );
+ if ( $subStatus->isOK() ) {
+ if ( $journaled ) { // journal log entries
+ $entries = array_merge( $entries,
+ $fileOp->getJournalEntries( $oldPredicates, $predicates ) );
+ }
+ } else { // operation failed?
+ $status->success[$index] = false;
+ ++$status->failCount;
+ if ( !$ignoreErrors ) {
+ wfProfileOut( __METHOD__ );
+ return $status; // abort
+ }
+ }
+ }
+ // Push the last sub-batch
+ if ( count( $curBatch ) ) {
+ $pPerformOps[] = $curBatch;
+ }
+
+ // Log the operations in the file journal...
+ if ( count( $entries ) ) {
+ $subStatus = $journal->logChangeBatch( $entries, $batchId );
+ if ( !$subStatus->isOK() ) {
+ wfProfileOut( __METHOD__ );
+ return $subStatus; // abort
+ }
+ }
+
+ if ( $ignoreErrors ) { // treat precheck() fatals as mere warnings
+ $status->setResult( true, $status->value );
+ }
+
+ // Attempt each operation (in parallel if allowed and possible)...
+ if ( count( $pPerformOps ) < count( $performOps ) ) {
+ self::runBatchParallel( $pPerformOps, $status );
+ } else {
+ self::runBatchSeries( $performOps, $status );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * Attempt a list of file operations in series.
+ * This will abort remaining ops on failure.
+ *
+ * @param $performOps Array
+ * @param $status Status
+ * @return bool Success
+ */
+ protected static function runBatchSeries( array $performOps, Status $status ) {
+ foreach ( $performOps as $index => $fileOp ) {
+ if ( $fileOp->failed() ) {
+ continue; // nothing to do
+ }
+ $subStatus = $fileOp->attempt();
+ $status->merge( $subStatus );
+ if ( $subStatus->isOK() ) {
+ $status->success[$index] = true;
+ ++$status->successCount;
+ } else {
+ $status->success[$index] = false;
+ ++$status->failCount;
+ // We can't continue (even with $ignoreErrors) as $predicates is wrong.
+ // Log the remaining ops as failed for recovery...
+ for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) {
+ $performOps[$i]->logFailure( 'attempt_aborted' );
+ }
+ return false; // bail out
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Attempt a list of file operations sub-batches in series.
+ *
+ * The operations *in* each sub-batch will be done in parallel.
+ * The caller is responsible for making sure the operations
+ * within any given sub-batch do not depend on each other.
+ * This will abort remaining ops on failure.
+ *
+ * @param $performOps Array
+ * @param $status Status
+ * @return bool Success
+ */
+ protected static function runBatchParallel( array $pPerformOps, Status $status ) {
+ $aborted = false;
+ foreach ( $pPerformOps as $performOpsBatch ) {
+ if ( $aborted ) { // check batch op abort flag...
+ // We can't continue (even with $ignoreErrors) as $predicates is wrong.
+ // Log the remaining ops as failed for recovery...
+ foreach ( $performOpsBatch as $i => $fileOp ) {
+ $performOpsBatch[$i]->logFailure( 'attempt_aborted' );
+ }
+ continue;
+ }
+ $statuses = array();
+ $opHandles = array();
+ // Get the backend; all sub-batch ops belong to a single backend
+ $backend = reset( $performOpsBatch )->getBackend();
+ // If attemptAsync() returns synchronously, it was either an
+ // error Status or the backend just doesn't support async ops.
+ foreach ( $performOpsBatch as $i => $fileOp ) {
+ if ( !$fileOp->failed() ) { // failed => already has Status
+ $subStatus = $fileOp->attemptAsync();
+ if ( $subStatus->value instanceof FileBackendStoreOpHandle ) {
+ $opHandles[$i] = $subStatus->value; // deferred
+ } else {
+ $statuses[$i] = $subStatus; // done already
+ }
+ }
+ }
+ // Try to do all the operations concurrently...
+ $statuses = $statuses + $backend->executeOpHandlesInternal( $opHandles );
+ // Marshall and merge all the responses (blocking)...
+ foreach ( $performOpsBatch as $i => $fileOp ) {
+ if ( !$fileOp->failed() ) { // failed => already has Status
+ $subStatus = $statuses[$i];
+ $status->merge( $subStatus );
+ if ( $subStatus->isOK() ) {
+ $status->success[$i] = true;
+ ++$status->successCount;
+ } else {
+ $status->success[$i] = false;
+ ++$status->failCount;
+ $aborted = true; // set abort flag; we can't continue
+ }
+ }
+ }
+ }
+ return $status;
+ }
+}
*/
public function __construct( array $config ) {
parent::__construct( $config );
+ if ( !MWInit::classExists( 'CF_Constants' ) ) {
+ throw new MWException( 'SwiftCloudFiles extension not installed.' );
+ }
// Required settings
$this->auth = new CF_Authentication(
$config['swiftUser'],
$this->getContainer( $container );
return true; // container exists
} catch ( NoSuchContainerException $e ) {
- } catch ( InvalidResponseException $e ) {
- } catch ( Exception $e ) { // some other exception?
- $this->logException( $e, __METHOD__, array( 'path' => $storagePath ) );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__, array( 'path' => $storagePath ) );
}
return false;
} catch ( NoSuchContainerException $e ) {
$status->fatal( 'backend-fail-create', $params['dst'] );
return $status;
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
return $status;
}
$obj->set_etag( md5( $params['content'] ) );
// Use the same content type as StreamFile for security
$obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] );
- // Actually write the object in Swift
- $obj->write( $params['content'] );
+ if ( !empty( $params['async'] ) ) { // deferred
+ $handle = $obj->write_async( $params['content'] );
+ $status->value = new SwiftFileOpHandle( $this, $params, 'Create', $handle );
+ } else { // actually write the object in Swift
+ $obj->write( $params['content'] );
+ }
} catch ( BadContentTypeException $e ) {
$status->fatal( 'backend-fail-contenttype', $params['dst'] );
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
}
return $status;
}
+ /**
+ * @see SwiftFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseCreate( CF_Async_Op $cfOp, Status $status, array $params ) {
+ try {
+ $cfOp->getLastResponse();
+ } catch ( BadContentTypeException $e ) {
+ $status->fatal( 'backend-fail-contenttype', $params['dst'] );
+ }
+ }
+
/**
* @see FileBackendStore::doStoreInternal()
* @return Status
} catch ( NoSuchContainerException $e ) {
$status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
return $status;
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
return $status;
}
$obj->set_etag( md5_file( $params['src'] ) );
// Use the same content type as StreamFile for security
$obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] );
- // Actually write the object in Swift
- $obj->load_from_filename( $params['src'], True ); // calls $obj->write()
+ if ( !empty( $params['async'] ) ) { // deferred
+ wfSuppressWarnings();
+ $fp = fopen( $params['src'], 'rb' );
+ wfRestoreWarnings();
+ if ( !$fp ) {
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ } else {
+ $handle = $obj->write_async( $fp, filesize( $params['src'] ), true );
+ $status->value = new SwiftFileOpHandle( $this, $params, 'Store', $handle );
+ $status->value->resourcesToClose[] = $fp;
+ }
+ } else { // actually write the object in Swift
+ $obj->load_from_filename( $params['src'], true ); // calls $obj->write()
+ }
} catch ( BadContentTypeException $e ) {
$status->fatal( 'backend-fail-contenttype', $params['dst'] );
} catch ( IOException $e ) {
$status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
}
return $status;
}
+ /**
+ * @see SwiftFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseStore( CF_Async_Op $cfOp, Status $status, array $params ) {
+ try {
+ $cfOp->getLastResponse();
+ } catch ( BadContentTypeException $e ) {
+ $status->fatal( 'backend-fail-contenttype', $params['dst'] );
+ } catch ( IOException $e ) {
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ }
+ }
+
/**
* @see FileBackendStore::doCopyInternal()
* @return Status
} catch ( NoSuchContainerException $e ) {
$status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
return $status;
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
return $status;
}
// (b) Actually copy the file to the destination
try {
- $sContObj->copy_object_to( $srcRel, $dContObj, $dstRel );
+ if ( !empty( $params['async'] ) ) { // deferred
+ $handle = $sContObj->copy_object_to_async( $srcRel, $dContObj, $dstRel );
+ $status->value = new SwiftFileOpHandle( $this, $params, 'Copy', $handle );
+ } else { // actually write the object in Swift
+ $sContObj->copy_object_to( $srcRel, $dContObj, $dstRel );
+ }
+ } catch ( NoSuchObjectException $e ) { // source object does not exist
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see SwiftFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseCopy( CF_Async_Op $cfOp, Status $status, array $params ) {
+ try {
+ $cfOp->getLastResponse();
} catch ( NoSuchObjectException $e ) { // source object does not exist
$status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doMoveInternal()
+ * @return Status
+ */
+ protected function doMoveInternal( array $params ) {
+ $status = Status::newGood();
+
+ list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+ if ( $srcRel === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+ return $status;
+ }
+
+ list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
+ if ( $dstRel === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+ return $status;
+ }
+
+ // (a) Check the source/destination containers and destination object
+ try {
+ $sContObj = $this->getContainer( $srcCont );
+ $dContObj = $this->getContainer( $dstCont );
+ if ( empty( $params['overwrite'] ) &&
+ $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
+ {
+ $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
+ return $status;
+ }
+ } catch ( NoSuchContainerException $e ) {
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ return $status;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ return $status;
+ }
+
+ // (b) Actually move the file to the destination
+ try {
+ if ( !empty( $params['async'] ) ) { // deferred
+ $handle = $sContObj->move_object_to_async( $srcRel, $dContObj, $dstRel );
+ $status->value = new SwiftFileOpHandle( $this, $params, 'Move', $handle );
+ } else { // actually write the object in Swift
+ $sContObj->move_object_to( $srcRel, $dContObj, $dstRel );
+ }
+ } catch ( NoSuchObjectException $e ) { // source object does not exist
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
}
return $status;
}
+ /**
+ * @see SwiftFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseMove( CF_Async_Op $cfOp, Status $status, array $params ) {
+ try {
+ $cfOp->getLastResponse();
+ } catch ( NoSuchObjectException $e ) { // source object does not exist
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ }
+ }
+
/**
* @see FileBackendStore::doDeleteInternal()
* @return Status
try {
$sContObj = $this->getContainer( $srcCont );
- $sContObj->delete_object( $srcRel );
+ if ( !empty( $params['async'] ) ) { // deferred
+ $handle = $sContObj->delete_object_async( $srcRel );
+ $status->value = new SwiftFileOpHandle( $this, $params, 'Delete', $handle );
+ } else { // actually write the object in Swift
+ $sContObj->delete_object( $srcRel );
+ }
} catch ( NoSuchContainerException $e ) {
$status->fatal( 'backend-fail-delete', $params['src'] );
} catch ( NoSuchObjectException $e ) {
if ( empty( $params['ignoreMissingSource'] ) ) {
$status->fatal( 'backend-fail-delete', $params['src'] );
}
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
}
return $status;
}
+ /**
+ * @see SwiftFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseDelete( CF_Async_Op $cfOp, Status $status, array $params ) {
+ try {
+ $cfOp->getLastResponse();
+ } catch ( NoSuchContainerException $e ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ } catch ( NoSuchObjectException $e ) {
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ }
+ }
+ }
+
/**
* @see FileBackendStore::doPrepareInternal()
* @return Status
return $status; // already exists
} catch ( NoSuchContainerException $e ) {
// NoSuchContainerException thrown: container does not exist
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
return $status;
}
array( $this->auth->username ) // write
) );
}
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
return $status;
}
// metadata, we can make use of that to avoid RTTs
$contObj->mw_wasSecured = true; // avoid useless RTTs
}
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
}
}
$contObj = $this->getContainer( $fullCont, true );
} catch ( NoSuchContainerException $e ) {
return $status; // ok, nothing to do
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
return $status;
}
$this->deleteContainer( $fullCont );
} catch ( NoSuchContainerException $e ) {
return $status; // race?
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( NonEmptyContainerException $e ) {
+ return $status; // race? consistency delay?
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
return $status;
}
}
);
} catch ( NoSuchContainerException $e ) {
} catch ( NoSuchObjectException $e ) {
- } catch ( InvalidResponseException $e ) {
+ } catch ( CloudFilesException $e ) { // some other exception?
$stat = null;
- } catch ( Exception $e ) { // some other exception?
- $stat = null;
- $this->logException( $e, __METHOD__, $params );
+ $this->handleException( $e, null, __METHOD__, $params );
}
return $stat;
$obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD request
$data = $obj->read( $this->headersFromParams( $params ) );
} catch ( NoSuchContainerException $e ) {
- } catch ( InvalidResponseException $e ) {
- } catch ( Exception $e ) { // some other exception?
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__, $params );
}
return $data;
return ( count( $container->list_objects( 1, null, $prefix ) ) > 0 );
} catch ( NoSuchContainerException $e ) {
return false;
- } catch ( InvalidResponseException $e ) {
- } catch ( Exception $e ) { // some other exception?
- $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__,
+ array( 'cont' => $fullCont, 'dir' => $dir ) );
}
return null; // error
}
}
} catch ( NoSuchContainerException $e ) {
- } catch ( InvalidResponseException $e ) {
- } catch ( Exception $e ) { // some other exception?
- $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__,
+ array( 'cont' => $fullCont, 'dir' => $dir ) );
}
return $dirs;
$after = end( $files ); // update last item
reset( $files ); // reset pointer
} catch ( NoSuchContainerException $e ) {
- } catch ( InvalidResponseException $e ) {
- } catch ( Exception $e ) { // some other exception?
- $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__,
+ array( 'cont' => $fullCont, 'dir' => $dir ) );
}
return $files;
} catch ( NoSuchContainerException $e ) {
$status->fatal( 'backend-fail-stream', $params['src'] );
return $status;
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-stream', $params['src'] );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
return $status;
}
$output = fopen( 'php://output', 'wb' );
$obj = new CF_Object( $cont, $srcRel, false, false ); // skip HEAD request
$obj->stream( $output, $this->headersFromParams( $params ) );
- } catch ( InvalidResponseException $e ) { // 404? connection problem?
- $status->fatal( 'backend-fail-stream', $params['src'] );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-stream', $params['src'] );
- $this->logException( $e, __METHOD__, $params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
}
return $status;
}
} catch ( NoSuchContainerException $e ) {
$tmpFile = null;
- } catch ( InvalidResponseException $e ) {
- $tmpFile = null;
- } catch ( Exception $e ) { // some other exception?
+ } catch ( CloudFilesException $e ) { // some other exception?
$tmpFile = null;
- $this->logException( $e, __METHOD__, $params );
+ $this->handleException( $e, null, __METHOD__, $params );
}
return $tmpFile;
return $hdrs;
}
+ /**
+ * @see FileBackendStore::doExecuteOpHandlesInternal()
+ * @return Array List of corresponding Status objects
+ */
+ protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
+ $statuses = array();
+
+ $cfOps = array(); // list of CF_Async_Op objects
+ foreach ( $fileOpHandles as $index => $fileOpHandle ) {
+ $cfOps[$index] = $fileOpHandle->cfOp;
+ }
+ $batch = new CF_Async_Op_Batch( $cfOps );
+
+ $cfOps = $batch->execute();
+ foreach ( $cfOps as $index => $cfOp ) {
+ $status = Status::newGood();
+ try { // catch exceptions; update status
+ $function = '_getResponse' . $fileOpHandles[$index]->call;
+ $this->$function( $cfOp, $status, $fileOpHandles[$index]->params );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status,
+ __CLASS__ . ":$function", $fileOpHandles[$index]->params );
+ }
+ $statuses[$index] = $status;
+ }
+
+ foreach ( $fileOpHandles as $fileOpHandle ) {
+ $fileOpHandle->closeResources();
+ }
+
+ return $statuses;
+ }
+
/**
* Set read/write permissions for a Swift container
*
* @param $container string Container name
* @param $bypassCache bool Bypass all caches and load from Swift
* @return CF_Container
+ * @throws NoSuchContainerException
* @throws InvalidResponseException
*/
protected function getContainer( $container, $bypassCache = false ) {
$info['bytes']
);
}
- } catch ( InvalidResponseException $e ) {
- } catch ( Exception $e ) { // some other exception?
- $this->logException( $e, __METHOD__, array() );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__, array() );
}
}
/**
- * Log an unexpected exception for this backend
+ * Log an unexpected exception for this backend.
+ * This also sets the Status object to have a fatal error.
*
* @param $e Exception
+ * @param $status Status|null
* @param $func string
* @param $params Array
* @return void
*/
- protected function logException( Exception $e, $func, array $params ) {
+ protected function handleException( Exception $e, $status, $func, array $params ) {
+ if ( $status instanceof Status ) {
+ if ( $e instanceof AuthenticationException ) {
+ $status->fatal( 'backend-fail-connect', $this->name );
+ } else {
+ $status->fatal( 'backend-fail-internal', $this->name );
+ }
+ }
+ if ( $e->getMessage() ) {
+ trigger_error( "$func: " . $e->getMessage(), E_USER_WARNING );
+ }
wfDebugLog( 'SwiftBackend',
get_class( $e ) . " in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
- ( $e instanceof InvalidResponseException
- ? ": {$e->getMessage()}"
- : ""
- )
+ ( $e->getMessage() ? ": {$e->getMessage()}" : "" )
);
}
}
+/**
+ * @see FileBackendStoreOpHandle
+ */
+class SwiftFileOpHandle extends FileBackendStoreOpHandle {
+ /** @var CF_Async_Op */
+ public $cfOp;
+
+ public function __construct( $backend, array $params, $call, CF_Async_Op $cfOp ) {
+ $this->backend = $backend;
+ $this->params = $params;
+ $this->call = $call;
+ $this->cfOp = $cfOp;
+ }
+}
+
/**
* SwiftFileBackend helper class to page through listings.
* Swift also has a listing limit of 10,000 objects for sanity.
}
/**
- * @return bool|int|null
+ * @return int log_id of the inserted log entry
*/
protected function saveContent() {
global $wgLogRestrictions;
} elseif( $this->sendToUDP ) {
# Don't send private logs to UDP
if( isset( $wgLogRestrictions[$this->type] ) && $wgLogRestrictions[$this->type] != '*' ) {
- return true;
+ return $newId;
}
# Notify external application via UDP.
* @param $params Array: parameters passed later to wfMsg.* functions
* @param $doer User object: the user doing the action
*
- * @return bool|int|null
- * @TODO: make this use LogEntry::saveContent()
+ * @return int log_id of the inserted log entry
*/
public function addEntry( $action, $target, $comment, $params = array(), $doer = null ) {
global $wgContLang;
* @return string
*/
protected function getRevisionLink() {
- $date = $this->list->getLanguage()->timeanddate( $this->revision->getTimestamp(), true );
+ $date = $this->list->getLanguage()->userTimeAndDate(
+ $this->revision->getTimestamp(), $this->list->getUser() );
if ( $this->isDeleted() && !$this->canViewContent() ) {
return $date;
}
*/
protected function getDiffLink() {
if ( $this->isDeleted() && !$this->canViewContent() ) {
- return wfMsgHtml('diff');
+ return $this->list->msg( 'diff' )->escaped();
} else {
return
Linker::link(
$this->list->title,
- wfMsgHtml('diff'),
+ $this->list->msg( 'diff' )->escaped(),
array(),
array(
'diff' => $this->revision->getId(),
protected function getRevisionLink() {
$undelete = SpecialPage::getTitleFor( 'Undelete' );
- $date = $this->list->getLanguage()->timeanddate( $this->revision->getTimestamp(), true );
+ $date = $this->list->getLanguage()->userTimeAndDate(
+ $this->revision->getTimestamp(), $this->list->getUser() );
if ( $this->isDeleted() && !$this->canViewContent() ) {
return $date;
}
protected function getDiffLink() {
if ( $this->isDeleted() && !$this->canViewContent() ) {
- return wfMsgHtml( 'diff' );
+ return $this->list->msg( 'diff' )->escaped();
}
$undelete = SpecialPage::getTitleFor( 'Undelete' );
- return Linker::link( $undelete, wfMsgHtml('diff'), array(),
+ return Linker::link( $undelete, $this->list->msg( 'diff' )->escaped(), array(),
array(
'target' => $this->list->title->getPrefixedText(),
'diff' => 'prev',
* @return string
*/
protected function getLink() {
- $date = $this->list->getLanguage()->timeanddate( $this->file->getTimestamp(), true );
+ $date = $this->list->getLanguage()->userTimeAndDate(
+ $this->file->getTimestamp(), $this->list->getUser() );
if ( $this->isDeleted() ) {
# Hidden files...
if ( !$this->canViewContent() ) {
$link = Linker::userLink( $this->file->user, $this->file->user_text ) .
Linker::userToolLinks( $this->file->user, $this->file->user_text );
} else {
- $link = wfMsgHtml( 'rev-deleted-user' );
+ $link = $this->list->msg( 'rev-deleted-user' )->escaped();
}
if( $this->file->isDeleted( Revision::DELETED_USER ) ) {
return '<span class="history-deleted">' . $link . '</span>';
if( $this->file->userCan( File::DELETED_COMMENT, $this->list->getUser() ) ) {
$block = Linker::commentBlock( $this->file->description );
} else {
- $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
+ $block = ' ' . $this->list->msg( 'rev-deleted-comment' )->escaped();
}
if( $this->file->isDeleted( File::DELETED_COMMENT ) ) {
return "<span class=\"history-deleted\">$block</span>";
public function getHTML() {
$data =
- wfMsg(
- 'widthheight',
- $this->list->getLanguage()->formatNum( $this->file->getWidth() ),
- $this->list->getLanguage()->formatNum( $this->file->getHeight() )
- ) .
- ' (' .
- wfMsgExt( 'nbytes', 'parsemag', $this->list->getLanguage()->formatNum( $this->file->getSize() ) ) .
- ')';
+ $this->list->msg( 'widthheight' )->numParams(
+ $this->file->getWidth(), $this->file->getHeight() )->text() .
+ ' (' . $this->list->msg( 'nbytes' )->numParams( $this->file->getSize() )->text() . ')';
return '<li>' . $this->getLink() . ' ' . $this->getUserTools() . ' ' .
$data . ' ' . $this->getComment(). '</li>';
}
protected function getLink() {
- $date = $this->list->getLanguage()->timeanddate( $this->file->getTimestamp(), true );
+ $date = $this->list->getLanguage()->userTimeAndDate(
+ $this->file->getTimestamp(), $this->list->getUser() );
$undelete = SpecialPage::getTitleFor( 'Undelete' );
$key = $this->file->getKey();
# Hidden files...
}
public function getHTML() {
- $date = htmlspecialchars( $this->list->getLanguage()->timeanddate( $this->row->log_timestamp ) );
+ $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate(
+ $this->row->log_timestamp, $this->list->getUser() ) );
$title = Title::makeTitle( $this->row->log_namespace, $this->row->log_title );
$formatter = LogFormatter::newFromRow( $this->row );
+ $formatter->setContext( $this->list->getContext() );
$formatter->setAudience( LogFormatter::FOR_THIS_USER );
// Log link for this page
$loglink = Linker::link(
SpecialPage::getTitleFor( 'Log' ),
- wfMsgHtml( 'log' ),
+ $this->list->msg( 'log' )->escaped(),
array(),
array( 'page' => $title->getPrefixedText() )
);
# Diffs
'history-title' => 'Historial de revisiones de "$1"',
+'difference-title' => 'Diferencia ente revisiones de «$1»',
+'difference-title-multipage' => 'Diferencia ente les páxines «$1» y «$2»',
'difference-multipage' => '(Diferencia ente páxines)',
'lineno' => 'Llinia $1:',
'compareselectedversions' => 'Comparar les revisiones seleicionaes',
# Diffs
'history-title' => 'Гісторыя зьменаў старонкі «$1»',
+'difference-title' => 'Розьніца паміж вэрсіямі «$1»',
+'difference-title-multipage' => 'Розьніца паміж старонкамі «$1» і «$2»',
'difference-multipage' => '(Розьніца паміж старонкамі)',
'lineno' => 'Радок $1:',
'compareselectedversions' => 'Параўнаць выбраныя вэрсіі',
'revdelete-uname-hid' => 'скрито потребителско име',
'revdelete-restricted' => 'добавени ограничения за администраторите',
'revdelete-unrestricted' => 'премахнати ограничения за администраторите',
-'logentry-move-move' => '$1 премести страницата $3 към заглавие $4',
-'logentry-move-move-noredirect' => '$1 премести страницата $3 към заглавие $4 без да оставя пренасочване',
+'logentry-move-move' => '$1 премести „$3“ като „$4“',
+'logentry-move-move-noredirect' => '$1 премести „$3“ като „$4“ без пренасочване',
'logentry-move-move_redir' => '$1 премести страницата $3 като $4 (върху пренасочване)',
-'logentry-move-move_redir-noredirect' => '$1 премести върху пренасочване страницата $3 към заглавие $4 (без пренасочване)',
+'logentry-move-move_redir-noredirect' => '$1 премести върху пренасочване „$3“ като „$4“ без пренасочване',
'logentry-patrol-patrol' => '$1 отбеляза като патрулирана версия $4 на страницата „$3“',
'logentry-patrol-patrol-auto' => '$1 автоматично отбеляза като патрулирана версия $4 на страницата $3',
'logentry-newusers-newusers' => '$1 създаде потребителска сметка',
Els seus motius han estat: «''$2''».",
'filereadonlyerror' => 'No s\'ha pogut modificar el fitxer «$1» perquè el repositori de fitxers "$2" està en mode només de lectura.
L\'administrador que l\'ha bloquejat ha donat aquesta explicació: "$3".',
+'invalidtitle-knownnamespace' => "El títol amb l'espai de noms «$2» i text «$3» no és vàlid",
+'invalidtitle-unknownnamespace' => "El títol amb l'espai de noms desconegut de número «$1» i text «$3» no és vàlid",
# Virus scanner
'virus-badscanner' => "Mala configuració: antivirus desconegut: ''$1''",
# Diffs
'history-title' => 'Historial de versions de «$1»',
+'difference-title' => 'Diferència entre les revisions de «$1»',
+'difference-title-multipage' => 'Diferència entre les pàgines «$1» i «$2»',
'difference-multipage' => '(Diferència entre pàgines)',
'lineno' => 'Línia $1:',
'compareselectedversions' => 'Compara les versions seleccionades',
'http-curl-error' => "Error en recuperar l'URL: $1",
'http-host-unreachable' => "No s'ha pogut accedir a l'URL.",
'http-bad-status' => 'Hi ha hagut un problema durant la petició HTTP: $1 $2',
+'http-truncated-body' => "El cos de la sol·licitud només s'ha rebut parcialment.",
# Some likely curl errors. More could be added from <http://curl.haxx.se/libcurl/c/libcurl-errors.html>
'upload-curl-error6' => "No s'ha pogut accedir a l'URL",
'confirmemail_pending' => 'Es wurde dir bereits ein Bestätigungscode per E-Mail zugeschickt.
Wenn du dein Benutzerkonto erst vor kurzem erstellt hast, warte bitte noch ein paar Minuten auf die E-Mail, bevor du einen neuen Code anforderst.',
'confirmemail_send' => 'Bestätigungscode zuschicken',
-'confirmemail_sent' => 'Bestätigungs-E-Mail wurde verschickt.',
+'confirmemail_sent' => 'Die Bestätigungs-E-Mail wurde verschickt.',
'confirmemail_oncreate' => 'Ein Bestätigungs-Code wurde an deine E-Mail-Adresse gesandt. Dieser Code wird für die Anmeldung nicht benötigt, jedoch wird er zur Aktivierung der E-Mail-Funktionen innerhalb des Wikis gebraucht.',
'confirmemail_sendfailed' => '{{SITENAME}} konnte die Bestätigungs-E-Mail nicht an dich versenden.
Bitte prüfe die E-Mail-Adresse auf ungültige Zeichen.
'node-count-exceeded-warning' => 'Bok jo licbu sukow pśekšocył',
'expansion-depth-exceeded-category' => 'Boki, źož ekspansiska dłymokosć jo pśekšocona',
'expansion-depth-exceeded-warning' => 'Bok jo ekspansisku dłymokosć pśekšocył',
+'parser-unstrip-loop-warning' => 'Njeskóńcna kokula namakana',
+'parser-unstrip-recursion-limit' => 'Rekursiska granica pśekšocona ($1)',
# "Undo" feature
'undo-success' => 'Wobźěłanje móžo se wótpóraś. Pšosym pśeglěduj dołojcne pśirownowanje aby se wěsty był, až to wót wěrnosći coš, a pón składuj změny, aby se wobźěłanje doskóńcnje wótpórało.',
# Diffs
'history-title' => 'Stawizny wersijow boka „$1“',
+'difference-title' => 'Rozdźěl mjazy wersijami "$1"',
+'difference-title-multipage' => 'Rozdźěl mjazy bokami "$1" a "$2"',
'difference-multipage' => '(Rozdźěl mjazy bokami)',
'lineno' => 'Rědka $1:',
'compareselectedversions' => 'Wuzwólonej wersiji pśirownaś',
'parser-template-loop-warning' => 'Rekursiva ŝablono estis trovita: [[$1]]',
'parser-template-recursion-depth-warning' => 'Limo de ŝablona profundeco pligrandiĝis ($1)',
'language-converter-depth-warning' => 'Profundo de lingvo-konvertilo preterpasis limon ($1)',
-'node-count-exceeded-category' => 'Paĝoj kie la nombro da nodoj estis preterpasita',
+'node-count-exceeded-category' => 'Paĝoj kie la nombro da nodoj estas preterpasita',
'node-count-exceeded-warning' => 'Paĝo preterpasis la nombron da nodoj.',
-'expansion-depth-exceeded-category' => 'Paĝoj en kiuj la ekpansiprofundo estis preterpasita.',
+'expansion-depth-exceeded-category' => 'Paĝoj en kiuj la ekpansiprofundo estas preterpasita',
'expansion-depth-exceeded-warning' => 'Paĝo preterpasis la ekpansiprofundon.',
# "Undo" feature
'saveusergroups' => 'Guardar grupos de usuarios',
'userrights-groupsmember' => 'Miembro de:',
'userrights-groupsmember-auto' => 'Miembro implícito de:',
-'userrights-groups-help' => 'Puedes modificar los grupos a los que pertenece este usuario:
-* Un recuadro marcado significa que el usuario está en ese grupo.
-* Un recuadro no marcado significa que el usuario no está en ese grupo.
-* Un * indica que no podrás retirar el grupo una vez que lo concedas, o viceversa.',
+'userrights-groups-help' => 'Puedes modificar los grupos a los que pertenece {{GENDER:$1|este usuario|esta usuaria}}:
+* Un recuadro marcado significa que {{GENDER:$1|el usuario|la usuaria}} está en ese grupo.
+* Un recuadro no marcado significa que {{GENDER:$1|el usuario|la usuaria}} no está en ese grupo.
+* Un * indica que no podrás eliminar el grupo una vez que lo agregues, o viceversa.',
'userrights-reason' => 'Motivo:',
'userrights-no-interwiki' => 'No tienes permiso para editar los grupos a los que pertenece un usuario en otros wikis.',
'userrights-nodatabase' => 'La base de datos $1 no existe o no es local.',
'ns-specialprotected' => 'Spetsjåålside koone ai beårbed wårde.',
'titleprotected' => 'En sid ma dideer noome koon ai önjläid wårde.
Jü späre wörd döör [[User:$1|$1]] ma grün "$2" inruchted.',
-'filereadonlyerror' => "Det datei „$1“ koon ei feranert wurd, auer uun det fertiaknis „$2“ bluat leesen wurd koon.
-
-Di grünj as „''$3''“.",
+'filereadonlyerror' => 'Det datei „$1“ koon ei feranert wurd, auer uun det fertiaknis „$2“ bluas leesen wurd koon.
+Di grünj faan di administraator as: „$3“.',
# Virus scanner
'virus-badscanner' => "Hiinje konfigurasjoon: ünbekånde fiirusscanner: ''$1''",
'subject' => 'Bedrååwet:',
'minoredit' => 'Bloot kleenihäide wörden feränred',
'watchthis' => 'Kiike eefter jüdeer sid',
-'savearticle' => 'Sid spikre',
+'savearticle' => 'Sidj seekre',
'preview' => 'Forlök',
'showpreview' => 'Forlök wise',
'showlivepreview' => 'Live-forkiik',
'tooltip-ca-nstab-help' => 'Heelpsid wise',
'tooltip-ca-nstab-category' => 'Kategoriisid wise',
'tooltip-minoredit' => 'Jüdeer änring as latj markiire.',
-'tooltip-save' => 'Änringe spikre',
+'tooltip-save' => 'Feranerangen seekre',
'tooltip-preview' => 'Forlök foon da änringe bai jüdeer sid. Hål for dåt spikern brüke!',
'tooltip-diff' => 'Änringe bai di täkst wise',
'tooltip-compareselectedversions' => 'Ferschääl twasche tou ütwäälde färsjoone foon jüdeer sid wise.',
Estes argumentos foron omitidos.",
'post-expand-template-argument-category' => 'Páxinas que conteñen argumentos de modelo omitidos',
'parser-template-loop-warning' => 'Detectouse un modelo en bucle: [[$1]]',
-'parser-template-recursion-depth-warning' => 'Excedeuse o límite da profundidade do recurso do modelo ($1)',
+'parser-template-recursion-depth-warning' => 'Excedeuse o límite de profundidade de recursión do modelo ($1)',
'language-converter-depth-warning' => 'Excedeuse o límite de profundidade do convertedor de lingua ($1)',
'node-count-exceeded-category' => 'Páxinas nas que se supera o número de nodos',
'node-count-exceeded-warning' => 'Páxina que supera o número de nodos',
'expansion-depth-exceeded-category' => 'Páxinas nas que se supera a profundidade de expansión',
'expansion-depth-exceeded-warning' => 'Páxina que supera a profundidade de expansión',
+'parser-unstrip-loop-warning' => 'Detectouse un bucle inamovible',
+'parser-unstrip-recursion-limit' => 'Excedeuse o límite de recursión inamovible ($1)',
# "Undo" feature
'undo-success' => 'A edición pódese desfacer.
# Diffs
'history-title' => 'Historial de revisións de "$1"',
+'difference-title' => 'Diferenzas entre revisións de "$1"',
+'difference-title-multipage' => 'Diferenzas entre as páxinas "$1" e "$2"',
'difference-multipage' => '(Diferenzas entre páxinas)',
'lineno' => 'Liña $1:',
'compareselectedversions' => 'Comparar as versións seleccionadas',
# Diffs
'history-title' => 'היסטוריית הגרסאות של $1',
+'difference-title' => 'הבדלים בין גרסאות של "$1"',
+'difference-title-multipage' => 'הבדלים בין הדפים $1 ו{{GRAMMAR:תחילית|$2}}',
'difference-multipage' => '(הבדלים בין דפים)',
'lineno' => 'שורה $1:',
'compareselectedversions' => 'השוואת הגרסאות שנבחרו',
'updated' => '(Ažurirano)',
'note' => "'''Napomena:'''",
'previewnote' => "'''Ne zaboravite da je ovo samo pregled kako će stranica izgledati i da stranica još nije snimljena!'''",
+'continue-editing' => 'Nastavi uređivati',
'previewconflict' => 'Ovaj pregled odražava stanje u gornjem polju za unos koje će biti sačuvano
ako pritisnete "Sačuvaj stranicu".',
'session_fail_preview' => "'''Ispričavamo se! Nismo mogli obraditi Vašu izmjenu zbog gubitka podataka o prijavi.
# Diffs
'history-title' => 'Stawizny wersijow strony „$1“',
+'difference-title' => 'Rozdźěl mjez wersijemi "$1"',
+'difference-title-multipage' => 'Rozdźěl mjez stronami "$1" a "$2"',
'difference-multipage' => '(Rozdźěl mjez stronami)',
'lineno' => 'Rjadka $1:',
'compareselectedversions' => 'Wubranej wersiji přirunać',
# Diffs
'history-title' => 'Historia de versiones de "$1"',
+'difference-title' => 'Differentia inter versiones de "$1"',
+'difference-title-multipage' => 'Differentia inter paginas "$1" e "$2"',
'difference-multipage' => '(Differentia inter paginas)',
'lineno' => 'Linea $1:',
'compareselectedversions' => 'Comparar versiones seligite',
'summary-preview' => "Anteprima dell'oggetto:",
'subject-preview' => 'Anteprima oggetto/intestazione:',
'blockedtitle' => 'Utente bloccato.',
-'blockedtext' => "'''Questo nome utente o indirizzo IP sono stati bloccati.'''
+'blockedtext' => "'''Il tuo nome utente o indirizzo IP è stato bloccato.'''
Il blocco è stato imposto da $1. La motivazione del blocco è la seguente: ''$2''
# Diffs
'history-title' => 'Cronologia delle modifiche di "$1"',
-'difference-title' => 'Differenza tra le revisioni di " $1 "',
-'difference-title-multipage' => 'Differenza tra le pagine " $1 "e" $2 "',
+'difference-title' => 'Differenza tra le versioni di "$1"',
+'difference-title-multipage' => 'Differenza tra le pagine "$1" e "$2"',
'difference-multipage' => '(Differenze fra le pagine)',
'lineno' => 'Riga $1:',
'compareselectedversions' => 'Confronta le versioni selezionate',
IP アドレスは複数の利用者で共有されている場合があります。
もし、あなたが匿名利用者であり、自分に関係のないコメントが寄せられている考えられる場合は、[[Special:UserLogin/signup|アカウントを作成する]]か[[Special:UserLogin|ログインして]]他の匿名利用者と間違えられないようにしてください。''",
'noarticletext' => '現在このページには内容がありません。
-他のページ内で[[Special:Search/{{PAGENAME}}|このページ名を検索する]]か、
+他のページ内で[[Special:Search/{{PAGENAME}}|このページ名を検索]]するか、
<span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 関連する記録を検索]するか、
-あるいは、[{{fullurl:{{FULLPAGENAME}}|action=edit}} このページを編集]</span>できます。',
+[{{fullurl:{{FULLPAGENAME}}|action=edit}} このページを編集]</span>することができます。',
'noarticletext-nopermission' => '現在このページには内容がありません。他のページに含まれる[[Special:Search/{{PAGENAME}}|このページ名を検索する]]か、もしくは<span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 関連記録を検索する]</span>ことができます。',
'userpage-userdoesnotexist' => '「$1」という利用者アカウントは登録されていません。
このページの作成/編集が適切かどうか確認してください。',
'mywatchlist' => 'ウォッチリスト',
'watchlistfor2' => '利用者: $1 $2',
'nowatchlist' => 'ウォッチリストに項目がありません。',
-'watchlistanontext' => 'ウォッチリストに入っている項目を表示または編集するには、$1してください。',
+'watchlistanontext' => 'ウォッチリストにある項目を閲覧または編集するには、$1してください。',
'watchnologin' => 'ログインしていません',
'watchnologintext' => 'ウォッチリストを変更するためには、[[Special:UserLogin|ログイン]]している必要があります。',
'addwatch' => 'ウォッチリストに追加',
'tooltip-n-mainpage' => 'メインページに移動',
'tooltip-n-mainpage-description' => 'メインページに移動する',
'tooltip-n-portal' => 'このプロジェクトについて、できること、情報を入手する場所',
-'tooltip-n-currentevents' => 'æ\9c\80è¿\91ã\81®å\87ºæ\9d¥äº\8bã\81«ã\81¤ã\81\84ã\81¦äº\88å\82\99ç\9f¥è\98ã\82\92å¾\97る',
+'tooltip-n-currentevents' => 'æ\9c\80è¿\91ã\81®å\87ºæ\9d¥äº\8bã\81®è\83\8cæ\99¯ã\82\92ç\9f¥る',
'tooltip-n-recentchanges' => 'このウィキにおける最近の更新の一覧',
-'tooltip-n-randompage' => '無作為に抽出されたページの読み込み',
+'tooltip-n-randompage' => '無作為に選ばれたページを読み込む',
'tooltip-n-help' => '情報を得る場所',
'tooltip-t-whatlinkshere' => 'ここにリンクしている全ウィキページの一覧',
'tooltip-t-recentchangeslinked' => 'このページからリンクしているページの最近の更新',
'filereadonlyerror' => 'ფაილი "$1" შეცვლა ვერ ხერხდება, რადგანაც ფაილის საცავი "$2" მხოლოდ კითხვის რეჟიმშია.
ადმინისტრატორი რომელმაც ის დაბლოკა მიუთითა შემდეგი მიზეზი: "$3".',
+'invalidtitle-knownnamespace' => 'დაუშვებელი სათაური სახელთა სივრცე "$2" და ტექსტი "$3"-თან',
+'invalidtitle-unknownnamespace' => 'დაუშვებელი სათაური უცნობი სახელთა სივრცის ნომერი $1 და ტექსტი "$2"-ით',
# Virus scanner
'virus-badscanner' => "შეცდომა. ვირუსთა უცნობი სკანერი: ''$1''",
'parser-template-loop-warning' => 'აღმოჩენილია ლუპი თარგ: [[$1]]-ში',
'parser-template-recursion-depth-warning' => 'თარგის რეკურსიის სიღრმე აემატება დაშვებულს ($1)',
'language-converter-depth-warning' => 'ენათა გადამუშავების კონვერციის ლიმიტი ამოწურულია ($1)',
+'node-count-exceeded-category' => 'გვერდები, რომელშიც გადამეტებულია კვანძების რაოდენობა',
+'node-count-exceeded-warning' => 'გვერდზე გადამეტებულია კვანძების რაოდენობა',
+'expansion-depth-exceeded-category' => 'გვერდები გახსნის სიღრმის გადამეტებით',
+'expansion-depth-exceeded-warning' => 'გვერდზე გადამეტებულია ჩადგმების ზღვარი',
+'parser-unstrip-recursion-limit' => 'გადამეტებულია რეკურსიის ზღვარი ($1)',
# "Undo" feature
'undo-success' => 'რედაქტირების გაუქმება შესაძლებელია. გთხოვთ შეამოწმოთ განსხვავება ქვევით, რათა დარწმუნდეთ, რომ ეს ის არის რაც თქვენ გსურთ, შემდეგ კი შეინახეთ ცვლილებები რათა დაასრულოთ რედაქტირების გაუქმება.',
# Diffs
'history-title' => 'ცვლილებათა ისტორია სტატიაში „$1“',
+'difference-title' => 'განსხვავება გადახედვებს შორის " $1 "',
+'difference-title-multipage' => 'განსხვავება „$1“ და „$2“ გვერდებს შორის',
'difference-multipage' => '(განსხვავება გვერდებს შორის)',
'lineno' => 'ხაზი $1:',
'compareselectedversions' => 'არჩეული ვერსიების შედარება',
'customjsprotected' => "Dir hutt net d'Recht dës JavaScript-Säit z'änneren, well dorop déi perséinlech Astellunge vun engem anere Benotzer gespäichert sinn.",
'ns-specialprotected' => 'Spezialsäite kënnen net verännert ginn.',
'titleprotected' => "Eng Säit mat dësem Numm kann net ugeluecht ginn. Dës Spär gouf vum [[User:$1|$1]] gemaach deen als Grond ''$2'' uginn huet.",
+'invalidtitle-knownnamespace' => 'Net valabelen Titel mam Nummraum "$2" a mam Text "$3"',
+'invalidtitle-unknownnamespace' => 'Net valabelen Titel mat der onbekannter Nummraum-Zuel $1 a mam Text "$2"',
# Virus scanner
'virus-badscanner' => "Schlecht Configuratioun: onbekannte Virescanner: ''$1''",
'minoreditletter' => 't',
'newpageletter' => 'T',
'boteditletter' => 'k',
+'newsectionsummary' => '/* $1 */ hläwm thar',
'rc-enhanced-expand' => 'Tilang kim rawh (JavaScript a ngai)',
'rc-enhanced-hide' => 'Thup ţhenna',
'rc-old-title' => 'Atìra "&1" tih hming pú-a siam.',
'upload-recreate-warning' => "'''Vaukhànna: Hemi hming pu taksa hi sawn tawh emaw paih tawh a ni.'''
I ràwnah paihna leh sawnna chhinchhiahna thuziak kan rawn chhawpchhuak e:",
+'uploadtext' => "Taksa hlankai nan a hnuaia lehkha hi hmang rawh.
+Ahmaa taksa hlankaisaho en tùr emaw zawng tùr chuan [[Special:FileList|taksa hlankai tawh zawng zawng tlarna]] tihah hian kal rawh; hlankai (nawn)ho pawh [[Special:Log/upload|hlankai chhinchhiahna]]-ah vawnfel an ni a, paih tawhho pawh [[Special:Log/delete|paihho chhinchhiahna]]-ah chhinchhiah fel vek an ni.
+
+Phêka taksa hmang tùrin ahnuaia tihphung engemaw ni ber hmang khuan zawmna i siam thei ang:
+*A taksa pumpui hmang tùr chuan '''<tt><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.jpg]]</nowiki></tt>''' tiin. Heti hi chuan taksa hi i sezawl chhuah a ni.
+*Phêk veilam síra 'Sawifiahna' hmanga 200px-a liana i tàrlan duh chuan '''<tt><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.png|200px|thumb|left|Sawifiahna]]</nowiki></tt>''' tiin.
+*A taksa tárlang lova zawm ringawt i duh chuan '''<tt><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></tt>''' tiin.",
'upload-permitted' => 'Taksa phal chiho: $1.',
'upload-preferred' => 'Taksa duh deuh bîk: $1.',
'upload-prohibited' => 'Taksa khap: $1.',
-'uploadlog' => 'chanchin hlankaina',
+'uploadlog' => 'hlankai chhinchhiahna',
'uploadlogpage' => 'Hlankai chhinchhiahna',
+'uploadlogpagetext' => 'Taksa hlankai thar deuh deuh kan rawn tlar chhuak e.
+A aia hmuhnawm deuh zâwka i thlìr duh chuan [[Special:NewFiles|taksa hlankai thar tàrhmunpui]]-ah i kal thei ang.',
'filename' => 'Taksahming',
-'filedesc' => 'Laktawi',
-'fileuploadsummary' => 'Kaihtawi:',
+'filedesc' => 'Sawifiahna täwi',
+'fileuploadsummary' => 'Sawifiahna täwi:',
'filereuploadsummary' => 'Taksa tihdanglamna:',
-'filestatus' => 'Copyright dinhmun:',
+'filestatus' => 'Lâkchhàwn phalphung dinhmun:',
'filesource' => 'Hnar:',
'uploadedfiles' => 'Taksa hlankai tawhte',
'ignorewarning' => 'Vaukhanna ngaihthah la taksa dahţha lui rawh',
A hming thlâk la bei ţha leh rawh.',
'filename-toolong' => 'Taksahming 240 bytes aiin a sei tùr a ni lo.',
'badfilename' => 'Taksahming "$1"-ah thlâk a ni.',
-'filetype-missing' => 'Taksa in tawpna a nei lo (entirna ".jpg").',
+'filetype-missing' => 'Taksain tawpna a nei lo (entirna ".jpg").',
'empty-file' => 'I taksa thehluh kha a ruak.',
'file-too-large' => 'A lian leh lutuk.',
'filename-tooshort' => 'Taksahming a sawi leh lutuk.',
'illegal-filename' => 'Taksahming phal loh.',
'overwrite' => 'Taksa awmsa ziah hnan khap a ni.',
'unknown-error' => 'Dikhlelhna hriat loh a lo thleng.',
+'tmp-create-error' => 'Taksa lailâwk a siam theih loh.',
+'tmp-write-error' => 'Taksa lailâwk ziah a tlawlh',
+'large-file' => 'Taksa rëng rëng $1 aia lian lo tùra duh a ni;
+he taksa hi $2 lái a ni.',
+'largefileserver' => 'Hë taksa hi rawngbawltu phaltir zât aia ritsak a ni.',
+'windows-nonascii-filename' => 'Hë wiki hian taksahminga chhinchhiahna bîk tel a pawm thei lo.',
+'savefile' => 'Taksa dahṭhatna',
'uploadedimage' => 'hlankai: "[[$1]]"',
+'upload-source' => 'Hnar taksa',
+'sourcefilename' => 'Hnar taksahming:',
+'sourceurl' => 'Hnar URL:',
+'destfilename' => 'Tumhmun taksahming:',
+'upload-maxfilesize' => 'Taksa lenzáwng bituk: $1 aia lian lo.',
+'upload-description' => 'Taksa sawifiahna',
+'upload-options' => 'Hlankai duhdàn thlanna',
+'watchthisupload' => 'Hë taksa hi vil rawh',
+'filewasdeleted' => 'He hming pu taksa hi ahmaah lo hlankai tawh a ni a, paihbo leh nghâl a ni.
+Hlankai leh i tum hmain $1 i thlïthläi deuh dawn nia.',
+'upload-success-subj' => 'Hlawhtling taka hlankai a ni',
+'upload-success-msg' => '[$2] aṭangin i hlangkai hlawhtling ta e. Hetah hian hman theihin a awm ta: [[:{{ns:file}}:$1]]',
+'upload-failure-subj' => 'Hlankai rokhawlhna',
+'upload-failure-msg' => '[$2] aṭanga i hlankai tumnaah rokhawlhna a awm tlat:
+
+$1',
+'upload-warning-subj' => 'Hlankai vauhkhànna',
+'upload-warning-msg' => '[$2] aṭanga i hlankaiah rokhawlhna a awm tlat. [[Special:Upload/stash/$1|Hlankaina lehkha]]-ah chingfel tùrin i lêt leh thei ang.',
-'license' => 'Phalna:',
-'license-header' => 'Phalna:',
+'upload-proto-error' => 'Inpawhphung dikhlel',
+'upload-file-error' => 'Chhúnglam dik lohna',
+
+'license' => 'Phalphung:',
+'license-header' => 'Phalphung:',
+'nolicense' => 'I la thlang lo',
+'license-nopreview' => '(Endik chhin theih loh)',
+'upload_source_url' => '(URL tláng pawh theih, nung bawk)',
+'upload_source_file' => '(i khawlthlûakneia mi taksa)',
# Special:ListFiles
-'imgfile' => 'taksa (file)',
+'listfiles-summary' => 'Hë vohbîk phêk hian taksa hlankai tawh zawng zawng a pholang.
+Hmangtu azira i thliarhran erawh chuan a hmangtuina a hlankai hnuhnüng ber taksa chauh pholan a ni.',
+'listfiles_search_for' => 'Media hming zawnna:',
+'imgfile' => 'taksa',
'listfiles' => 'Taksa tlarna',
'listfiles_thumb' => 'Kutbï',
'listfiles_date' => 'Ni',
'filehist-missing' => 'Taksa a awm lo',
'imagelinks' => 'Bungraw hmanna',
'linkstoimage' => 'A hnuai {{PLURAL:$1|phek 1|phek $1-te}} hian he taksa hi a hmang:',
+'linkstoimage-more' => 'Hë taksa hi phêk {{PLURAL:$1||}} $1 aia tamin a zawm/hmang.
+Ahnuaih hian {{PLURAL:$1|zawmtu hmasa ber|zawmtu hmasa $1-te}} kan rawn tlar chhuak e.
+Zawmtu zawng zawng [[Special:WhatLinksHere/$2|tlarchhuahna hetah hian a awm]] e.',
'nolinkstoimage' => 'He taksa zawmtu/hmanna phêk pakhat mah a awm lo.',
+'morelinkstoimage' => 'Hemi taksa zawmpui dang [[Special:WhatLinksHere/$1|enna}}.',
+'linkstoimage-redirect' => '$1 (taksa hruailuhna) $2',
+'sharedupload' => 'Hë taksa hi $1-a mi a ni a, hna-hmachhawp dangin a hmang vè mai thei.',
+'sharedupload-desc-there' => 'Hë taksa hi $1-a mi a ni a, hna-hmachhawp dangin a hmang ve mai thei.
+Hriattirna dang chu [$2 taksa sawifiahna phêk]-ah hian i en thei ang.',
'sharedupload-desc-here' => 'He taksa hi $1-a mi a ni a, hna-hmachhawp dangin an hmang ve mai thei.
[Taksa sawifiahna phek $2]-a sawifiahna lang hetah hian kan rawh chhawp chhuak e.',
+'sharedupload-desc-edit' => 'Hë taksa hi $1-a mi a ni a, hna-hmachhawp dangin a hmang vè mai thei.
+Taksa sawifiahna hi i siamṭha duh a nih chuan [$2 taksa sawifiahna phêk] aṭang hian i siamṭha thei ang.',
'filepage-nofile' => 'He hmingpu taksa a awm lo',
'filepage-nofile-link' => 'He hming pu taksa hi a awm lo va, mahsé i [$1 hlangkai thei] ang.',
'uploadnewversion-linktext' => 'He taksa chhuah thar hi hlangkai rawh',
# Statistics
'statistics' => 'Lepsena',
'statistics-pages' => 'Phekte',
-
+'statistics-pages-desc' => 'Hë wiki-a phêk awm zawng zawng, sawihona phêk, hruailuhna phêk ladt. tel vekin.',
+'statistics-files' => 'Taksa hlankaite',
+'statistics-edits' => '{{SITENAME}} din achina phêk khawih danglam zât',
+'statistics-edits-average' => 'Phêk khat zëla siamṭhat zât chawhrual',
+'statistics-views-total' => 'Tlawh zât',
+'statistics-views-total-desc' => 'Phêk awm lo leh phêk vohbîkte tlawhna chu chhiar tel a ni lo',
+'statistics-views-peredit' => 'Siamṭhat pakhat zël tlawh zât',
+'statistics-users' => '[[Special:ListUsers|Hmangtu]] inziaklût zât',
+'statistics-users-active' => 'Hmangtu aktif zât',
+'statistics-users-active-desc' => 'Ni {{PLURAL:$1|khat|$1}} kaltâ chhunga thiltih nei hmangtuho',
+'statistics-mostpopular' => 'Phêk thlir zin deuh deuhte',
+
+'disambiguations' => 'Thliarfelna phêk zawmtu phêkte',
'disambiguationspage' => 'Template:thliar',
+'doubleredirects' => 'Hruailuhna phír',
+'double-redirect-fixed-move' => '[[$1]] sawn a ni ta.
+[[$2]] lama hruailuhna siam nghâl a ni.',
+'double-redirect-fixed-maintenance' => '[[$1]] aṭanga [[$2]] hruailuhna phír chinfel.',
+'double-redirect-fixer' => 'Hruailuhna chingfeltu',
+
+'brokenredirects' => 'Hruailuhna kehchhia',
+'brokenredirectstext' => 'Ahnuaia hruailuhnate hian phêk awm lova hruailuh an tum:',
'brokenredirects-edit' => 'siamţhatna',
'brokenredirects-delete' => 'paihna',
# Miscellaneous special pages
'nbytes' => 'Bait {{PLURAL:$1||}} $1',
-'nmembers' => 'Tel (a chhunga awm) {{PLURAL:$1||$1}}',
+'nmembers' => 'A chhungah {{PLURAL:$1||$1}} a awm.',
+'nrevisions' => 'siamṭhatna $1 {{PLURAL:$1||}}',
+'nviews' => 'vawi $1 {{PLURAL:$1||}} ràwn a ni tawh.',
+'nimagelinks' => 'Phêk $1-ah {{PLURAL:$1||}} hman a ni.',
+'ntransclusions' => 'phêk $1-ah {{PLURAL:$1||}} hman a ni.',
+'specialpage-empty' => 'Thuhawn a awm lo tlat.',
+'lonelypages' => 'Phêk fahrahte',
+'uncategorizedpages' => 'Páwl nei lo phêkte',
+'uncategorizedcategories' => 'Páwl nei lo páwlte',
+'uncategorizedimages' => 'Páwl nei lo taksate',
+'uncategorizedtemplates' => 'Páwl nei lo siamsâte',
+'unusedcategories' => 'Páwl hman lohte',
+'unusedimages' => 'Taksa hman hlawh lote',
'popularpages' => 'Phêk lärte',
-'prefixindex' => 'Hmabet nei phek zawng zawng',
+'wantedcategories' => 'Páwl mamawhte',
+'wantedpages' => 'Phêk mamawhte',
+'wantedpages-badtitle' => 'Phêk hming dik lo: $1',
+'wantedfiles' => 'Taksa mamawhte',
+'wantedtemplates' => 'Siamsa mamawhte',
+'mostlinked' => 'Phêk zawm hlawh berte',
+'mostlinkedcategories' => 'Pawl zawm hlawh berte',
+'mostlinkedtemplates' => 'Siamsa hman hlawh berte',
+'mostcategories' => 'Telna páwl ngah ber phêkte',
+'mostimages' => 'Taksa hman hlawh berte',
+'mostrevisions' => 'Phêk siam danglam zin berte',
+'prefixindex' => 'Hemi hmabet nei phek zawng zawng',
+'prefixindex-namespace' => 'Hemi ($1 hminghmun) hmabeta neih phêk zawng zawngte',
+'shortpages' => 'Phêk täwite',
+'longpages' => 'Phêk seite',
+'deadendpages' => 'Phêk ralthümte',
+'protectedpages' => 'Phêk vènhimte',
+'protectedpages-indef' => 'Phêk vènhim kumhlunho chauh',
+'protectedpagestext' => 'A hnuaia phêkte hi sawn emaw siam danglam theih loh tùra vènhim an ni',
+'protectedtitles' => 'Hming vènhimte',
+'listusers' => 'Hmangtu tlarna',
+'listusers-editsonly' => 'Siam danglam nei hmangtu chauh pholang rawh',
+'listusers-creationsort' => 'Siam ni indawtin tlar rawh',
+'usereditcount' => 'Siam danglam {{PLURAL:||}} $1',
'usercreated' => '{{GENDER:$3|}} Ni $1, dar $2-a siam',
'newpages' => 'Phek tharte',
'newpages-username' => 'Hmangtu hming:',
'ancientpages' => 'Phek hluiho',
'move' => 'Sawnna',
+'movethispage' => 'Hë phêk hi sawn rawh',
+'notargettitle' => 'Tumhmun nei lo',
+'nopagetitle' => 'Hetiang tumhmun phêk hi a awm lo',
+'nopagetext' => 'I tumhmun sawi kher kha a awm lo niin a lang.',
'pager-newer-n' => '{{PLURAL:$1|thar zawk 1|thar zawk $1}}',
'pager-older-n' => '{{PLURAL:$1|hlui 1|hlui $1}}',
+'suppress' => 'Hmuh hmaih',
# Book sources
'booksources' => 'Lehkhabu rawnte',
'booksources-go' => 'Kal rawh le',
# Special:Log
+'specialloguserlabel' => 'Buatsaihtu:',
+'speciallogtitlelabel' => 'Tumhmun (hming emaw hmangtu):',
'log' => 'Chanchin-ziak',
+'all-logs-page' => 'Chanchin-ziak vàntlang thilte:',
+'log-title-wildcard' => 'Hë thüa inṭan hmingte hi zawng rawh',
# Special:AllPages
'allpages' => 'Phek zawng zawngte',
-'alphaindexline' => '$1 aţanga $2',
+'alphaindexline' => '$1 aṭanga $2',
'nextpage' => 'Phek dawt ($1)',
'prevpage' => 'Phêk hmasa ($1)',
+'allpagesfrom' => 'Hemi aṭanga inṭan hian pholang rawh:',
+'allpagesto' => 'Hemi chin thleng hian:',
'allarticles' => 'Phek zawng zawngte',
'allinnamespace' => 'Phêk zawng zawng (hminghmun $1-a mi)',
'allnotinnamespace' => 'Phêk zawng zawng (hminghmun $1-a awm lo)',
'allpagesprefix' => 'Hemi thuhmabeta neih zawng hi pholang rawh:',
'allpagesbadtitle' => 'Phêk hming dik lo emaw ţawng dang/wiki dang thuhmabet a hmang palh a nih hmel.
Phêk hminga hman awih loh hawrawp a hmang palh a ni mai thei bawk.',
+'allpages-bad-ns' => '{{SITENAME}} hian "$1" tih hminghmun a nei lo.',
+'allpages-hide-redirects' => 'Hruailuhna phêkho thup rawh',
+
+# SpecialCachedPage
+'cachedspecial-refresh-now' => 'A thar ber thlirna.',
# Special:Categories
'categories' => 'Pawlte',
+'categoriespagetext' => 'A hnuaia pawl{{PLURAL:$1||te}} hian phêk emaw media a{{PLURAL:$1||n}} nei.
+Hetah hian [[Special:UnusedCategories|pawl hman lohho]] pholan tel a ni lo.
+[[Special:WantedCategories|Pawl mamawhho]] en bawk la.',
+'categoriesfrom' => 'Hemi-a inṭan pawlho hi pholang rawh:',
+'special-categories-sort-count' => 'a chhúnga thil awm zât azirin thliar rawh',
'special-categories-sort-abc' => 'a-aw-b indawtin',
# Special:DeletedContributions
+'deletedcontributions' => 'Hmangtu kutthawhna paihbo tawhte',
+'deletedcontributions-title' => 'Hmangtu kutthawhna paihbo tawhte',
'sp-deletedcontributions-contribs' => 'kutthawhnate',
# Special:LinkSearch
+'linksearch' => 'Zawmchhuahna zawnna',
+'linksearch-pat' => 'Thu zawn:',
'linksearch-ns' => 'Hminghmun:',
-'linksearch-ok' => 'Zawng rawh',
+'linksearch-ok' => 'Zawng rawh le',
'linksearch-line' => '$1 hi $2 aţanga thlunzawm a ni',
+# Special:ListUsers
+'listusersfrom' => 'Hemi-a inṭanin hmangtuho pholang rawh:',
+'listusers-submit' => 'Pholanna',
+'listusers-noresult' => 'Hmangtu an awm lo.',
+'listusers-blocked' => '(danbeh)',
+
+# Special:ActiveUsers
+'activeusers' => 'Hmangtu hlun tlarna',
+'activeusers-hidebots' => 'Khawlmi thupna',
+'activeusers-hidesysops' => 'Roreltu thupna',
+'activeusers-noresult' => 'Hmangtu awm lo.',
+
# Special:Log/newusers
'newuserlogpage' => 'Hmangtu siamna chanchin-ziak',
+'newuserlogpagetext' => 'Hei hi hmangtu siangchan siam chhinchhiahna a ni.',
# Special:ListGroupRights
+'listgrouprights' => 'Hmangtu pawl dikna-chanvote',
+'listgrouprights-key' => '* <span class="listgrouprights-granted">Dikna-chanvo phalsak</span>
+* <span class="listgrouprights-revoked">Dikna-chanvo hnuhkirsak</span>',
+'listgrouprights-group' => 'Pawl',
+'listgrouprights-rights' => 'Dikna-chanvo',
+'listgrouprights-helppage' => 'Help:Pawl dikna-chanvo',
'listgrouprights-members' => '(tel zawng zawng)',
+'listgrouprights-addgroup' => '{{PLURAL:$2|Pawl|Pawl}} belhna: $1',
# E-mail user
'emailuser' => 'He hmangtu hi e-lehkha thawn rawh',
'unwatch' => 'Vil tihtawpna',
'unwatchthispage' => 'Vil tihtawpna',
'notanarticle' => 'Phêk dikdawh a ni lo.',
+'notvisiblerev' => 'Hmangtu dang tihdanglamna thar paihbo a ni.',
'watchnochange' => 'I hun sawi chhungah khan i ralvèn khawih buai a ni lo.',
'watchlist-details' => 'I ralvèn zing aţanga {{PLURAL:$1| phêk $1 |phêk $1}}, sawihona phêk chhiar lohvin.',
'wlheader-enotif' => '*E-lehkha inhriattirna tihnun a ni.',
Chu tlara zawmna dang awm reng reng chu bîk-thil-a ngaih a ni ang; tlar chhunga taksa awmna tùr phek kan tihna a nih chu.',
# Metadata
-'metadata' => 'Nepnawi (metadata)',
-'metadata-help' => 'He taksa hian hriatna dang a keng tel, thlalakna emaw ami siamna sekrek ilo aţanga lo awm a ni mai thei.
-He taksa hi a tira a nihphung tihdanglam tawh a nih chuan a chanchin ziah khuan a danglam hnu chanchin a huam tel lo mai thei.',
+'metadata' => 'Nepnawi',
+'metadata-help' => 'He taksa hian hriatna dang a keng tel; thlalakna emaw thlachhuina amah siamna sekrek ilo aṭanga lo awm a ni mai thei.
+He taksa hi a tira a nihphung tihdanglam tawh a nih chuan a chanchin ziah khuan a danglam hnu chanchin a hril kim lo mai thei.',
+'metadata-expand' => 'Chanchin kimchang tihlanna',
+'metadata-collapse' => 'Chanchin kimchang thup ṭhenna',
'metadata-fields' => 'Thlalâk chanchin nepnawi he thuthawna tihlante hi thlalâk phek pholan huna chanchin nepnawi dawhkan thleh a nihin tihlan a ni ang.
A bak zawng chu thuhrûk sa vek a ni ang.
* make
* gpsaltitude',
# EXIF tags
+'exif-imagewidth' => 'Zàuzáwng',
+'exif-imagelength' => 'Sànzáwng',
+'exif-bitspersample' => 'Péng khata mal(bit) awm zât',
+'exif-compression' => 'Sàwrtêtphung',
+'exif-photometricinterpretation' => 'Rawng inpawlhphung',
+'exif-orientation' => 'Hawizáwng',
+'exif-samplesperpixel' => 'Péng zât',
+'exif-planarconfiguration' => 'Hriatna inremphung',
+'exif-ycbcrsubsampling' => 'Y leh C inthlauhbï',
+'exif-ycbcrpositioning' => 'Y leh C ṭhuthmun',
+'exif-xresolution' => 'Pheizáwnga tihfiahphung',
+'exif-yresolution' => 'Tungzáwnga tihfiahphung',
+'exif-stripoffsets' => 'Thlalâk hriattirna awmna',
+'exif-rowsperstrip' => 'Ṭhuang khata tlar zât',
+'exif-stripbytecounts' => 'Ṭhuang zàwr zìmtina byte awm zât',
+'exif-jpeginterchangeformat' => 'JPEG SOI dahhmun',
+'exif-jpeginterchangeformatlength' => 'JPEG hriattirna lenzawng (Byte-in)',
+'exif-whitepoint' => 'Hmun vár rawng nihphung',
+'exif-primarychromaticities' => 'Rawng bulbälte nihphung',
+'exif-datetime' => 'Taksa tihdanglam hun leh ni',
+'exif-imagedescription' => 'Lem hming',
+'exif-make' => 'Thlalâkna siamtu',
+'exif-model' => 'Thlalâkna sìamphung',
+'exif-software' => 'Khawlthlûak hman',
+'exif-artist' => 'Buatsaihtu',
+'exif-copyright' => 'Phalna kawltu',
+'exif-exifversion' => 'EXIF chhuah',
+'exif-flashpixversion' => 'Flashpix chhuah chhawmdàwl theih',
+'exif-colorspace' => 'Rawng hmun',
+'exif-componentsconfiguration' => 'Péngtinte awmzia',
+'exif-compressedbitsperpixel' => 'Lem sàwrzìmphung',
+'exif-pixelydimension' => 'Lem zauzáwng',
+'exif-pixelxdimension' => 'Lem sànzáwng',
+'exif-usercomment' => 'Hmangtu kamchhuak',
+'exif-relatedsoundfile' => 'Ritaksa laichinte',
+'exif-datetimeoriginal' => 'Hriattirna siamchhuah hun leh ni',
+'exif-datetimedigitized' => 'Tihkhàwl hun leh ní',
+'exif-subsectime' => 'Tihdanglam hun leh ni',
+'exif-exposuretime' => 'Phochhuah hun',
+'exif-fnumber' => 'F zât',
'exif-source' => 'Hnar:',
'exif-writer' => 'Ziaktu',
'exif-languagecode' => 'Ţawng',
'node-count-exceeded-warning' => 'Op de pagina is het maximale aantal nodes overschreden',
'expansion-depth-exceeded-category' => "Pagina's waar de expansiediepte is overschreden",
'expansion-depth-exceeded-warning' => 'De pagina bevat te veel sjablonen',
+'parser-unstrip-loop-warning' => 'Er is een "unstrip"-lus gedetecteerd',
+'parser-unstrip-recursion-limit' => 'De recursielimiet ($1) voor "unstrip" is overschreden',
# "Undo" feature
'undo-success' => 'Deze bewerking kan ongedaan gemaakt worden.
* @author Kaganer
* @author Lajsikonik
* @author Lampak
+ * @author Lazowik
* @author Leinad
* @author Maikking
* @author Marcin Łukasz Kiejzik
# Diffs
'history-title' => 'Historia edycji „$1”',
+'difference-title' => 'Różnica pomiędzy wersjami strony "$1"',
+'difference-title-multipage' => 'Różnica pomiędzy stronami "$1" i "$2"',
'difference-multipage' => '(Różnica między stronami)',
'lineno' => 'Linia $1:',
'compareselectedversions' => 'porównaj wybrane wersje',
'blocklog-showsuppresslog' => 'Este utilizador foi bloqueado e ocultado anteriomente.
O registo de supressão é fornecido abaixo para referência:',
'blocklogentry' => 'bloqueou "[[$1]]" $3. O bloqueio expira em $2.',
-'reblock-logentry' => 'modificou parâmetros de bloqueio de [[$1]] $3. O bloqueio expira em $2.',
+'reblock-logentry' => 'modificou parâmetros de bloqueio de [[$1]] com expiração em $2. $3',
'blocklogtext' => 'Este é um registo de ações de bloqueio e desbloqueio.
Endereços IP sujeitos a bloqueio automático não estão listados.
Consulte a [[Special:BlockList|lista de bloqueios]] para obter a lista de bloqueios e banimentos atualmente válidos.',
'protectthispage' => 'Proteger esta página',
'unprotect' => 'Alterar a proteção',
'unprotectthispage' => 'Alterar a proteção desta página',
-'newpage' => 'Nova página',
+'newpage' => 'Página nova',
'talkpage' => 'Dialogar sobre esta página',
'talkpagelinktext' => 'disc',
'specialpage' => 'Página especial',
'filereadonlyerror' => 'Não é possível modificar o arquivo "$1" porque o repositório do arquivo "$2" está em modo somente leitura.
O administrador que bloqueou ofereceu a seguinte explicação: "$3".',
+'invalidtitle-knownnamespace' => 'Título inválido para o espaço nominal "$2" e texto "$3"',
+'invalidtitle-unknownnamespace' => 'Título inválido para o espaço nominal de número desconhecido ($1) e texto "$2"',
# Virus scanner
'virus-badscanner' => "Má configuração: antivírus desconhecido: ''$1''",
'yourname' => 'Nome de usuário:',
'yourpassword' => 'Senha:',
'yourpasswordagain' => 'Redigite sua senha',
-'remembermypassword' => 'Recordar os meus dados neste computador (por no máximo $1 {{PLURAL:$1|dia|dias}})',
+'remembermypassword' => 'Lembrar meu login neste navegador (por no máximo $1 {{PLURAL:$1|dia|dias}})',
'securelogin-stick-https' => 'Permanecer conectado ao HTTPS após a autenticação',
'yourdomainname' => 'Seu domínio:',
'externaldberror' => 'Ocorreu ou um erro no banco de dados durante a autenticação ou não lhe é permitido atualizar a sua conta externa.',
'login' => 'Autenticar-se',
'nav-login-createaccount' => 'Entrar / criar conta',
'loginprompt' => 'É necessário estar com cookies ativados para poder autenticar-se no wiki {{SITENAME}}.',
-'userlogin' => 'Criar uma conta ou entrar',
+'userlogin' => 'Entrar / criar conta',
'userloginnocreate' => 'Entrar',
'logout' => 'Sair',
-'userlogout' => 'Sair',
+'userlogout' => 'Desconectar',
'notloggedin' => 'Não autenticado',
'nologin' => 'Não possui uma conta? $1.',
'nologinlink' => 'Criar uma conta',
'login-throttled' => 'Você fez tentativas demais de se autenticar com esta conta recentemente.
Por favor aguarde antes de tentar novamente.',
'login-abort-generic' => 'A sua autenticação não teve êxito - Abortada',
-'loginlanguagelabel' => 'Língua: $1',
+'loginlanguagelabel' => 'Idioma: $1',
'suspicious-userlogout' => 'Sua solicitação para sair foi negada porque aparentemente foi enviada por um navegador danificado ou por um servidor proxy com cache.',
# E-mail sending
'link_tip' => 'Link interno',
'extlink_sample' => 'http://www.example.com título do link',
'extlink_tip' => 'Link externo (lembre-se do prefixo http://)',
-'headline_sample' => 'Texto do cabeçalho',
+'headline_sample' => 'Conteúdo do cabeçalho',
'headline_tip' => 'Seção de nível 2',
'nowiki_sample' => 'Inserir texto não-formatado aqui',
-'nowiki_tip' => 'Ignorar formato wiki',
+'nowiki_tip' => 'Ignorar a formatação wiki',
'image_sample' => 'Exemplo.jpg',
'image_tip' => 'Arquivo embutido',
'media_sample' => 'Exemplo.ogg',
-'media_tip' => 'Link para arquivo',
+'media_tip' => 'Link para o arquivo',
'sig_tip' => 'Sua assinatura, com hora e data',
'hr_tip' => 'Linha horizontal (use de forma moderada)',
'showpreview' => 'Mostrar previsão',
'showlivepreview' => 'Pré-visualização em tempo real',
'showdiff' => 'Mostrar alterações',
-'anoneditwarning' => "'''Atenção''': Você não se encontra autenticado. O seu endereço de IP será registrado no histórico de edições desta página.",
+'anoneditwarning' => "'''Atenção''': Você não se encontra autenticado.
+O seu endereço de IP será registrado no histórico de edições desta página.",
'anonpreviewwarning' => "''Você não está logado. Gravar registará o seu endereço IP no histórico de edições desta página.''",
'missingsummary' => "'''Lembrete:''' Você não introduziu um sumário de edição. Se clicar novamente em Salvar, a sua edição será salva sem um sumário.",
'missingcommenttext' => 'Por favor, introduzida um comentário abaixo.',
A senha para esta nova conta pode ser alterada na página ''[[Special:ChangePassword|de troca de senha]]'', após a autenticação.",
'newarticle' => '(Nova)',
-'newarticletext' => "Você seguiu um link para uma página que não existe.
-Para criá-la, comece escrevendo na caixa abaixo
-(veja [[{{MediaWiki:Helppage}}|a página de ajuda]] para mais informações).
-Se você chegou aqui por engano, apenas clique no botão '''voltar''' do seu navegador.",
+'newarticletext' => "Você seguiu um link para uma página que ainda não existe.
+Para criá-la, comece escrevendo na caixa abaixo (veja [[{{MediaWiki:Helppage}}|a página de ajuda]] para mais informações).
+Se você chegou aqui por engano, clique no botão '''voltar''' do seu navegador.",
'anontalkpagetext' => "---- ''Esta é a página de discussão para um usuário anônimo que ainda não criou uma conta ou que não a usa, de forma que temos de utilizar o endereço de IP para identificá-lo(a). Tal endereço de IP pode ser compartilhado por vários usuários. Se você é um usuário anônimo e acha que comentários irrelevantes foram direcionados a você, por gentileza, [[Special:UserLogin/signup|crie uma conta]] ou [[Special:UserLogin|autentique-se]], a fim de evitar futuras confusões com outros usuários anônimos.''",
'noarticletext' => 'No momento, não há conteúdo nesta página.
-Você pode [[Special:Search/{{PAGENAME}}|pesquisar pelo título desta página]] em outras páginas <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar por registros relacionados],
+Você pode [[Special:Search/{{PAGENAME}}|pesquisar pelo título desta página]] em outras páginas, <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar por registros relacionados],
ou [{{fullurl:{{FULLPAGENAME}}|action=edit}} criar esta página]</span>.',
-'noarticletext-nopermission' => 'Não há actualmente texto nesta página.
-Você pode [[Special:Search/{{PAGENAME}}|procurar este título de página]] em outras páginas,
-ou <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} procurar os registos relacionados] </span>.',
+'noarticletext-nopermission' => 'No momento, não há conteúdo nesta página
+Você pode [[Special:Search/{{PAGENAME}}|pesquisar pelo título desta página]] em outras páginas,
+ou <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar por registros relacionados] </span>.',
'userpage-userdoesnotexist' => 'A conta "<nowiki>$1</nowiki>" não se encontra registrada.
Verifique se deseja mesmo criar/editar esta página.',
'userpage-userdoesnotexist-view' => 'A conta de usuário "$1" não está registrada.',
'templatesusedsection' => '{{PLURAL:$1|Predefinição utilizada|Predefinições utilizadas}} nesta seção:',
'template-protected' => '(protegida)',
'template-semiprotected' => '(semi-protegida)',
-'hiddencategories' => 'Esta página integra {{PLURAL:$1|uma categoria oculta|$1 categorias ocultas}}:',
+'hiddencategories' => 'Esta página pertence a {{PLURAL:$1|uma categoria oculta|$1 categorias ocultas}}:',
'edittools' => '<!-- O texto aqui disponibilizado será exibido abaixo dos formulários de edição e de envio de arquivos. -->',
'nocreatetitle' => 'A criação de páginas se encontra limitada',
'nocreatetext' => '{{SITENAME}} tem restringida a habilidade de criar novas páginas.
'permissionserrors' => 'Erros de permissões',
'permissionserrorstext' => 'Você não possui permissão de fazer isso, {{PLURAL:$1|pelo seguinte motivo|pelos seguintes motivos}}:',
'permissionserrorstext-withaction' => 'Você não possui permissão para $2, {{PLURAL:$1|pelo seguinte motivo|pelos motivos a seguir}}:',
-'recreate-moveddeleted-warn' => "Atenção: Você está recriando uma página já eliminada em outra ocasião.'''
+'recreate-moveddeleted-warn' => "'''Atenção: Você está recriando uma página já eliminada em outra ocasião.'''
-Você deve considerar se é realmente adequado continuar editando esta página.
+Considere se é realmente adequado continuar editando esta página.
Os registros de eliminação e de movimentação desta página são exibidos a seguir, para sua comodidade:",
'moveddeleted-notice' => 'Esta página foi eliminada.
Os registros de eliminação e de movimentação para esta página estão disponibilizados abaixo, para referência.',
'viewpagelogs' => 'Ver registros para esta página',
'nohistory' => 'Não há histórico de revisões para esta página.',
'currentrev' => 'Revisão atual',
-'currentrev-asof' => 'Edição atual tal como $1',
+'currentrev-asof' => 'Edição atual tal como às $1',
'revisionasof' => 'Edição das $1',
'revision-info' => 'Edição feita às $1 por $2',
'previousrevision' => '← Edição anterior',
'last' => 'ult',
'page_first' => 'primeira',
'page_last' => 'última',
-'histlegend' => "Seleção para diferenças: marque as caixas de seleção das versões que deseja comparar e clique no botão na parte inferior.<br />
-Legenda: ''({{int:cur}})''' = diferença com relação a versão atual,
-'''({{int:last}})''' = diferença com relação a versão anterior, '''{{int:minoreditletter}}''' = edição menor.",
+'histlegend' => "Como selecionar: marque as caixas de seleção das versões que deseja comparar e pressione enter ou clique no botão na parte inferior do formulário.<br />
+Legenda: '''({{int:cur}})''' = diferenças em relação a última versão, '''({{int:last}})''' = diferenças em relação a versão anterior, '''{{int:minoreditletter}}''' = edição menor.",
'history-fieldset-title' => 'Navegar pelo histórico',
-'history-show-deleted' => 'Somente eliminados',
+'history-show-deleted' => 'Apenas as eliminadas',
'histfirst' => 'Mais antigas',
'histlast' => 'Mais recentes',
'historysize' => '({{PLURAL:$1|1 byte|$1 bytes}})',
Você pode ver a diferença entre revisões; podem existir mais detalhes no [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registo de eliminações].",
'rev-suppressed-diff-view' => "Uma das revisões desta comparação foi '''suprimida''''.
Você pode ver esta comparação; detalhes podem ser encontradas no [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} registro de supressão].",
-'rev-delundel' => 'mostrar/esconder',
+'rev-delundel' => 'exibir/ocultar',
'rev-showdeleted' => 'exibir',
'revisiondelete' => 'Eliminar/restaurar edições',
'revdelete-nooldid-title' => 'Nenhuma revisão selecionada',
# Diffs
'history-title' => 'Histórico de edições de "$1"',
+'difference-title' => 'Mudanças entre as edições de "$1"',
+'difference-title-multipage' => 'Mudanças entre as páginas "$1" e "$2"',
'difference-multipage' => '(Diferenças entre páginas)',
'lineno' => 'Linha $1:',
'compareselectedversions' => 'Compare as versões selecionadas',
'shown-title' => 'Mostrar $1 {{PLURAL:$1|resultado|resultados}} por página',
'viewprevnext' => 'Ver ($1 {{int:pipe-separator}} $2) ($3).',
'searchmenu-legend' => 'Opções de pesquisa',
-'searchmenu-exists' => "*'''Há a página \"[[:\$1]]\" neste wiki.'''",
+'searchmenu-exists' => "'''Há uma página com o nome \"[[:\$1]]\" neste wiki'''",
'searchmenu-new' => "'''Criar a página \"[[:\$1|\$1]]\" neste wiki!'''",
'searchhelp-url' => 'Help:Conteúdos',
'searchmenu-prefix' => '[[Special:PrefixIndex/$1|Navegue pelas páginas com este prefixo]]',
'search-result-score' => 'Relevância: $1%',
'search-redirect' => '(redirecionamento de $1)',
'search-section' => '(seção $1)',
-'search-suggest' => 'Será que quis dizer: $1',
+'search-suggest' => 'Você quis dizer: $1',
'search-interwiki-caption' => 'Projetos irmãos',
'search-interwiki-default' => 'Resultados de $1:',
'search-interwiki-more' => '(mais)',
'showingresultsheader' => "{{PLURAL:$5|Resulado '''$1''' de '''$3'''|Resultados '''$1 - $2''' de '''$3'''}} para '''$4'''",
'nonefound' => "'''Nota''': apenas alguns espaços nominais são pesquisados por padrão.
Tente utilizar o prefixo ''all:'' em sua busca, para pesquisar por todos os conteúdos deste wiki (inclusive páginas de discussão, predefinições etc), ou mesmo, utilizando o espaço nominal desejado como prefixo.",
-'search-nonefound' => 'Não houve resultados para a pesquisa.',
+'search-nonefound' => 'Não há resultados que correspondam à consulta.',
'powersearch' => 'Pesquisa avançada',
'powersearch-legend' => 'Pesquisa avançada',
'powersearch-ns' => 'Pesquisar nos espaços nominais:',
'recentchanges' => 'Mudanças recentes',
'recentchanges-legend' => 'Opções das mudanças recentes',
'recentchangestext' => 'Acompanhe, a partir desta página, as alterações recentes no wiki {{SITENAME}}.',
-'recentchanges-feed-description' => 'Acompanhe, a partir desta feed, as alterações mais recentes no wiki.',
+'recentchanges-feed-description' => 'Acompanhe neste feed as mudanças mais recentes do wiki.',
'recentchanges-label-newpage' => 'Esta edição criou uma nova página',
'recentchanges-label-minor' => 'Esta é uma edição menor',
-'recentchanges-label-bot' => 'Esta edição foi feita por um robô',
+'recentchanges-label-bot' => 'Esta edição foi feita por um bot',
'recentchanges-label-unpatrolled' => 'Esta edição ainda não foi patrulhada',
'rcnote' => "A seguir {{PLURAL:$1|está listada '''uma''' alteração ocorrida|estão listadas '''$1''' alterações ocorridas}} {{PLURAL:$2|no último dia|nos últimos '''$2''' dias}}, a partir das $5 de $4.",
-'rcnotefrom' => "Abaixo estão as alterações desde as '''$4''' de '''$3''' (limitadas a '''$1''').",
+'rcnotefrom' => "Seguem as alterações desde as '''$4''' de '''$3''' (limitadas a '''$1''').",
'rclistfrom' => 'Mostrar as novas alterações a partir das $1',
'rcshowhideminor' => '$1 edições menores',
-'rcshowhidebots' => '$1 robôs',
+'rcshowhidebots' => '$1 bots',
'rcshowhideliu' => '$1 usuários registrados',
'rcshowhideanons' => '$1 usuários anônimos',
'rcshowhidepatr' => '$1 edições patrulhadas',
'rcshowhidemine' => '$1 minhas edições',
-'rclinks' => 'Exibir as $1 alterações recentes dos últimos $2 dias<br />$3',
+'rclinks' => 'Exibir as $1 alterações recentes feitas nos últimos $2 dias<br />$3',
'diff' => 'dif',
'hist' => 'hist',
'hide' => 'Ocultar',
'recentchangeslinked-feed' => 'Alterações relacionadas',
'recentchangeslinked-toolbox' => 'Alterações relacionadas',
'recentchangeslinked-title' => 'Alterações relacionadas com "$1"',
-'recentchangeslinked-noresult' => 'Não ocorreram alterações em páginas relacionadas no intervalo de tempo fornecido.',
+'recentchangeslinked-noresult' => 'Não ocorreram alterações em páginas relacionadas no intervalo de tempo especificado.',
'recentchangeslinked-summary' => "Esta página lista alterações feitas recentemente em páginas com links a uma em específico (ou de membros de uma categoria especificada).
Páginas de sua [[Special:Watchlist|lista de páginas vigiadas]] são exibidas em '''negrito'''.",
'recentchangeslinked-page' => 'Nome da página:',
-'recentchangeslinked-to' => 'Mostrar alterações a páginas relacionadas com a página fornecida',
+'recentchangeslinked-to' => 'Visualizar as alterações nas páginas vinculadas à página especificada ao invés disso',
# Upload
'upload' => 'Enviar arquivo',
'upload-curl-error28' => 'Tempo limite para o envio do arquivo excedido',
'upload-curl-error28-text' => 'O site demorou muito tempo para responder. Por gentileza, verifique se o site está acessível, aguarde alguns momentos e tente novamente. Talvez você deseje fazer nova tentativa em um horário menos congestionado.',
-'license' => 'Licença:',
+'license' => 'Licenciamento:',
'license-header' => 'Licenciamento',
'nolicense' => 'Nenhuma selecionada',
'license-nopreview' => '(Previsão não disponível)',
# File description page
'file-anchor-link' => 'Arquivo',
'filehist' => 'Histórico do arquivo',
-'filehist-help' => 'Clique em uma data/horário para ver o arquivo tal como ele se encontrava em tal momento.',
+'filehist-help' => 'Clique em uma data/horário para ver como o arquivo estava em um dado momento.',
'filehist-deleteall' => 'eliminar todas',
'filehist-deleteone' => 'eliminar',
'filehist-revert' => 'restaurar',
'linkstoimage-more' => 'Mais de $1 {{PLURAL:$1|página|páginas}} tem algum link para este arquivo.
A lista a seguir mostra apenas {{PLURAL:$1|o primeiro link|os $1 primeiros links}} para este arquivo.
Uma [[Special:WhatLinksHere/$2|listagem completa]] está disponível.',
-'nolinkstoimage' => 'Nenhuma página aponta para este arquivo.',
+'nolinkstoimage' => 'Nenhuma página contém links para este arquivo.',
'morelinkstoimage' => 'Ver [[Special:WhatLinksHere/$1|mais links]] para este arquivo.',
'linkstoimage-redirect' => '$1 (redirecionamento de arquivo) $2',
'duplicatesoffile' => '{{PLURAL:$1|O seguinte arquivo é duplicado|Os seguintes arquivos são duplicados}} deste arquivo ([[Special:FileDuplicateSearch/$2|mais detalhes]]):',
'sharedupload-desc-there' => 'Este arquivo é do $1 e pode ser utilizado por outros projetos.
Por favor veja a [$2 página de descrição do arquivo] para mais informações.',
'sharedupload-desc-here' => 'Este arquivo é do $1 e pode ser utilizado por outros projetos.
-A descrição na sua [$2 página de descrição de arquivo] é exibida abaixo.',
+Sua [$2 página de descrição de arquivo] é reproduzida abaixo.',
'sharedupload-desc-edit' => 'Este arquivo é do $1 e pode ser utilizado por outros projetos.
Talvez você deseje editar a descrição na sua [$2 página de descrição de arquivo] por lá.',
'sharedupload-desc-create' => 'Este arquivo é do $1 e pode ser utilizado por outros projetos.
'notargettext' => 'Você não especificou uma página alvo ou um usuário para executar esta função.',
'nopagetitle' => 'Página alvo não existe',
'nopagetext' => 'A página alvo especificada não existe.',
-'pager-newer-n' => '{{PLURAL:$1|1 recente|$1 recentes}}',
-'pager-older-n' => '{{PLURAL:$1|1 antiga|$1 antigas}}',
+'pager-newer-n' => '{{PLURAL:$1|posterior|$1 posteriores}}',
+'pager-older-n' => '{{PLURAL:$1|1 anterior|$1 anteriores}}',
'suppress' => 'Supervisor',
'querypage-disabled' => 'Esta página especial está desativada para não prejudicar o desempenho.',
# Book sources
-'booksources' => 'Fontes de livros',
-'booksources-search-legend' => 'Procurar por fontes de livrarias',
+'booksources' => 'Fontes bibliográficas',
+'booksources-search-legend' => 'Procurar fontes bibliográficas',
'booksources-go' => 'Ir',
'booksources-text' => 'É exibida a seguir uma listagem de links para outros sites que vendem livros novos e usados e que possam possuir informações adicionais sobre os livros que você está pesquisando:',
'booksources-invalid-isbn' => 'O número ISBN fornecido não parece ser válido; verifique se houve erros ao copiar da fonte original.',
# Special:AllPages
'allpages' => 'Todas as páginas',
-'alphaindexline' => '$1 até $2',
+'alphaindexline' => 'De $1 até $2',
'nextpage' => 'Próxima página ($1)',
'prevpage' => 'Página anterior ($1)',
-'allpagesfrom' => 'Mostrar páginas começando em:',
-'allpagesto' => 'Terminar de exibir páginas em:',
+'allpagesfrom' => 'Primeira página na listagem:',
+'allpagesto' => 'Última página na listagem:',
'allarticles' => 'Todas as páginas',
'allinnamespace' => 'Todas as páginas (espaço nominal $1)',
'allnotinnamespace' => 'Todas as páginas (excepto as do espaço nominal $1)',
'linksearch-text' => 'É possível usar caracteres curinga, como "*.wikipedia.org".
É necessário, pelo menos, um domínio de nível superior, por exemplo "*.org".<br />
Protocolos suportados: <tt>$1</tt> (não adicionado nenhum desses em sua pesquisa).',
-'linksearch-line' => '$1 está lincado a partir de $2',
+'linksearch-line' => '$2 possui links para $1',
'linksearch-error' => "\"Caracteres mágicos\" (''wildcards'') só podem ser suados no início do endereço.",
# Special:ListUsers
'historywarning' => "'''Atenção:''' A página que você está prestes a eliminar possui um histórico com aproximadamente $1 {{PLURAL:$1|revisão|revisões}}:",
'confirmdeletetext' => 'Encontra-se prestes a eliminar permanentemente uma página ou uma imagem e todo o seu histórico.
Por favor, confirme que possui a intenção de fazer isto, que compreende as consequências e que encontra-se a fazer isto de acordo com as [[{{MediaWiki:Policy-url}}|políticas]] do projeto.',
-'actioncomplete' => 'Ação completada',
-'actionfailed' => 'A ação falhou',
+'actioncomplete' => 'Ação concluída',
+'actionfailed' => 'Falha na ação',
'deletedtext' => '"$1" foi eliminada.
Consulte $2 para um registro de eliminações recentes.',
'dellogpage' => 'Registro de eliminação',
'blanknamespace' => '(Principal)',
# Contributions
-'contributions' => 'Contribuições do usuário',
-'contributions-title' => 'Contribuições do usuário $1',
+'contributions' => 'Contribuições {{GENDER:{{BASEPAGENAME}}|do usuário|da usuária}}',
+'contributions-title' => 'Contribuições {{GENDER:$1|do usuário|da usuária}} $1',
'mycontris' => 'Minhas contribuições',
'contribsub2' => 'Para $1 ($2)',
'nocontribs' => 'Não foram encontradas mudanças com este critério.',
-'uctop' => ' (revisão atual)',
+'uctop' => '(atual)',
'month' => 'Mês (inclusive anteriores):',
'year' => 'Ano (inclusive anteriores):',
-'sp-contributions-newbies' => 'Mostrar só as contribuições das contas recentes',
+'sp-contributions-newbies' => 'Mostrar apenas as contribuições das novas contas',
'sp-contributions-newbies-sub' => 'Para contas novas',
'sp-contributions-newbies-title' => 'Contribuições de contas novas',
'sp-contributions-blocklog' => 'Registro de bloqueios',
'sp-contributions-deleted' => 'contribuições eliminadas',
-'sp-contributions-uploads' => 'carregamentos',
+'sp-contributions-uploads' => 'envios',
'sp-contributions-logs' => 'registros',
'sp-contributions-talk' => 'disc',
'sp-contributions-userrights' => 'gerenciamento de privilégios de usuários',
'sp-contributions-blocked-notice' => 'Este usuário atualmente está bloqueado. O registro de bloqueio mais recente é fornecido abaixo para referência:',
'sp-contributions-blocked-notice-anon' => 'Este endereço IP encontra-se bloqueado.
Segue, para referência, a entrada mais recente no registro de bloqueios:',
-'sp-contributions-search' => 'Pesquisar contribuições',
+'sp-contributions-search' => 'Navegar pelas contribuições',
'sp-contributions-username' => 'Endereço de IP ou usuário:',
-'sp-contributions-toponly' => 'Mostrar somente as revisões mais recentes',
+'sp-contributions-toponly' => 'Mostrar somente as edições que sejam a última alteração',
'sp-contributions-submit' => 'Pesquisar',
# What links here
'whatlinkshere' => 'Páginas afluentes',
-'whatlinkshere-title' => 'Páginas que apontam para "$1"',
+'whatlinkshere-title' => 'Páginas que têm links para "$1"',
'whatlinkshere-page' => 'Página:',
'linkshere' => "As seguintes páginas possuem links para '''[[:$1]]''':",
-'nolinkshere' => "Não existem links para '''[[:$1]]'''.",
+'nolinkshere' => "Não há links para '''[[:$1]]'''.",
'nolinkshere-ns' => "Não há links para '''[[:$1]]''' no espaço nominal selecionado.",
'isredirect' => 'página de redirecionamento',
-'istemplate' => 'inclusão',
+'istemplate' => 'transclusão',
'isimage' => 'link para o arquivo',
'whatlinkshere-prev' => '{{PLURAL:$1|anterior|$1 anteriores}}',
'whatlinkshere-next' => '{{PLURAL:$1|próximo|próximos $1}}',
O registro de bloqueio é fornecido abaixo, para referência:',
'blocklog-showsuppresslog' => 'O usuário foi bloqueado e ocultado anteriormente.
O registro de supressão é fornecido abaixo para referência:',
-'blocklogentry' => 'bloqueou "[[$1]]" $3. O bloqueio expira em $2.',
-'reblock-logentry' => 'modificou parâmetros de bloqueio de [[$1]] $3. O bloqueio expira em $2.',
+'blocklogentry' => 'bloqueou "[[$1]]" por $2. $3',
+'reblock-logentry' => 'modificou parâmetros de bloqueio de [[$1]] com expiração em $2. $3',
'blocklogtext' => 'Este é um registro de ações de bloqueio e desbloqueio.
Endereços IP sujeitos a bloqueio automático não são listados.
Consulte a [[Special:BlockList|lista de bloqueios]] para obter a lista de bloqueios e banimentos em efeito neste momento.',
'javascripttest-pagetext-frameworks' => 'Escolha uma das seguintes estruturas de teste: $1',
'javascripttest-pagetext-skins' => 'Escolha o tema para executar os testes:',
'javascripttest-qunit-intro' => 'Veja a [$1 documentação de testes] no mediawiki.org.',
+'javascripttest-qunit-heading' => 'Suíte de ferramentas de teste JavaScript QUnit para MediaWiki',
# Tooltip help for the actions
'tooltip-pt-userpage' => 'Sua página de usuário',
'tooltip-ca-nstab-main' => 'Ver a página de conteúdo',
'tooltip-ca-nstab-user' => 'Ver a página de usuário',
'tooltip-ca-nstab-media' => 'Ver a página de mídia',
-'tooltip-ca-nstab-special' => 'Esta é uma página especial, não pode ser editada.',
+'tooltip-ca-nstab-special' => 'Esta é uma página especial. Não é possível editar seu conteúdo de forma direta.',
'tooltip-ca-nstab-project' => 'Ver a página de projeto',
'tooltip-ca-nstab-image' => 'Ver a página de arquivo',
'tooltip-ca-nstab-mediawiki' => 'Ver a mensagem de sistema',
'tooltip-ca-nstab-template' => 'Ver a predefinição',
'tooltip-ca-nstab-help' => 'Ver a página de ajuda',
-'tooltip-ca-nstab-category' => 'Ver a página da categoria',
-'tooltip-minoredit' => 'Marcar como edição menor',
-'tooltip-save' => 'Salvar as alterações',
-'tooltip-preview' => 'Prever as alterações, por favor utilizar antes de salvar!',
-'tooltip-diff' => 'Mostrar alterações que fez a este texto.',
-'tooltip-compareselectedversions' => 'Ver as diferenças entre as duas versões selecionadas desta página.',
+'tooltip-ca-nstab-category' => 'Ver a página descritiva da categoria',
+'tooltip-minoredit' => 'Marcar esta alteração como uma edição menor',
+'tooltip-save' => 'Salva as suas alterações',
+'tooltip-preview' => 'Prevê as alterações feitas por você. Antes de salvar, use para ver se está tudo como esperado!',
+'tooltip-diff' => 'Visualizar as alterações que você fez no texto',
+'tooltip-compareselectedversions' => 'Ver o que há de diferente entre as duas versões selecionadas desta página.',
'tooltip-watch' => 'Adicionar esta página à sua lista de páginas vigiadas',
'tooltip-watchlistedit-normal-submit' => 'Remover títulos',
'tooltip-watchlistedit-raw-submit' => 'Atualizar a lista de páginas vigiadas',
'file-info-size-pages' => '$1 × $2 pixels, tamanho do arquivo: $3, tipo MIME: $4, $5 {{PLURAL:$5|página|páginas}}',
'file-nohires' => 'Sem resolução maior disponível.',
'svg-long-desc' => 'arquivo SVG, de $1 × $2 pixels, tamanho: $3',
-'show-big-image' => 'Resolução completa',
+'show-big-image' => 'Resolução original',
'show-big-image-preview' => 'Tamanho desta previsualização: $1.',
'show-big-image-other' => '{{PLURAL:$2|Outra resolução|Outras resoluções}}: $1.',
'show-big-image-size' => '$1 × $2 pixels',
# Metadata
'metadata' => 'Metadados',
-'metadata-help' => "Este arquivo contém informação adicional, provavelmente adicionada a partir da câmara digital ou ''scanner'' utilizada para criar ou digitalizá-lo.
-Caso o arquivo tenha sido modificado a partir do seu estado original, alguns detalhes poderão não refletir completamente as mudanças efetuadas.",
+'metadata-help' => 'Este arquivo contém dados adicionais, provavelmente adicionados pela câmera digital ou scanner utilizado para criar ou digitalizá-lo.
+Caso o arquivo tenha sofrido alterações, alguns detalhes poderão ser diferentes do que o arquivo atual é.',
'metadata-expand' => 'Mostrar detalhes adicionais',
'metadata-collapse' => 'Esconder detalhes restantes',
-'metadata-fields' => 'Os campos de metadados de imagens listados nesta mensagem serão incluídos na página de descrição da imagem quando a tabela de metadados estiver recolhida. Por omissão, outros campos estarão ocultos.
+'metadata-fields' => 'Os campos de metadados de imagens listados nesta mensagem serão incluídos na página de descrição da imagem quando a tabela de metadados estiver recolhida.
+Por padrão, outros campos estarão ocultos.
* make
* model
* datetimeoriginal
'exif-urgency-other' => 'Prioridade definida pelo usuário ($1)',
# External editor support
-'edit-externally' => 'Editar este arquivo utilizando uma aplicação externa',
+'edit-externally' => 'Editar este arquivo a partir de um programa externo',
'edit-externally-help' => '(Consulte as [//www.mediawiki.org/wiki/Manual:External_editors instruções de instalação] para maiores informações)',
# 'all' in various places, this might be different for inflected languages
'watchlistall2' => 'todas',
-'namespacesall' => 'todas',
+'namespacesall' => 'todos',
'monthsall' => 'todos',
'limitall' => 'todas',
'version-software' => 'Software instalado',
'version-software-product' => 'Produto',
'version-software-version' => 'Versão',
+'version-entrypoints' => 'URLs dos pontos de entrada',
+'version-entrypoints-header-entrypoint' => 'Ponto de entrada',
'version-entrypoints-header-url' => 'URL',
# Special:FilePath
'intentionallyblankpage' => 'Esta página foi intencionalmente deixada em branco e é usada para medições de performance, etc.',
# External image whitelist
-'external_image_whitelist' => " # Deixe esta linha exatamente como ela é <pre>
-# Coloque uma expressão regular (apenas a parte que vai entre o //) a seguir
+'external_image_whitelist' => " # Deixe esta linha exatamente como ela está <pre>
+# Insira uma expressão regular (apenas a parte que vai entre o //) a seguir
# Estas serão casadas com as URLs de imagens externas (''hotlinked'')
-# Aqueles que corresponderem serão exibidos como imagens, caso contrário, apenas uma ligação para a imagem será mostrada
+# Aquelas que corresponderem serão exibidas como imagens; caso contrário, apenas um link para a imagem será mostrado
# As linhas que começam com # são tratadas como comentários
# Isto não é sensível à capitalização
-# Coloque todos os fragmentos de ''regex'' acima dessa linha. Deixe esta linha exatamente como ela é</pre>",
+# Coloque todos os fragmentos de ''regex'' acima dessa linha. Deixe esta linha exatamente como ela está</pre>",
# Special:Tags
'tags' => 'Etiquetas de modificação válidas',
-'tag-filter' => 'Filtro de [[Special:Tags|etiquetas]]:',
+'tag-filter' => 'Filtrar [[Special:Tags|etiquetas]]:',
'tag-filter-submit' => 'Filtrar',
'tags-title' => 'Etiquetas',
'tags-intro' => 'Esta página lista as etiquetas com que o software poderá marcar uma edição, e o seu significado.',
* @file
*
* @author Albinus
+ * @author David Baskey
* @author Ghonokuashabaskey
+ * @author Joseph Mardy
* @author Nipon087
* @author Salvator
* @author Samar88
$messages = array(
# User preference toggles
'tog-hideminor' => 'Nitaḱ bodolaḱre huḍiṅ kạmi danaṅme',
+'tog-extendwatchlist' => 'Khạli nitoḱ bodolko do baṅ, joto bodolkodo ńeloḱ tạlikare phaylaomẽ.',
'tog-showtoc' => 'Ṭibilre menaḱako ńel ( sakamkore 3 khon jạti hedlayenko)',
'tog-watchcreations' => 'Sakamko songe Ińaḱ ńelok tạlikare benao',
+'tog-watchdefault' => 'Sakam tońgey me Iń do ińaḱ ńeloḱ tạlikare joṛaokeda',
+'tog-watchmoves' => 'Sakamko tońgeyme Ińaḱ ńelok tạlikare kulme',
+'tog-watchdeletion' => 'Sakamko tońgeyme Ińaḱ ńeloḱ tạlika khon get́ giḍikam',
'tog-enotifwatchlistpages' => 'E-mailạńme one tinre in̕aḱ n̕eloḱ tạlika do bodolok',
'tog-enotifusertalkpages' => 'E-mailạn̕me one tinre in̕aḱ roṛaḱ laṛcaṛ sakam do bodoloḱa',
'tog-enotifminoredits' => 'E-mailạn̕me arhõ one tinre in̕aḱ sakamre huḍiń kạmi hoyoḱ',
'create-this-page' => 'Noa sakam benao me',
'delete' => 'muchau me',
'deletethispage' => 'Noa sakam do get giḍikam',
-'undelete_short' => 'Bań get giḍika',
+'undelete_short' => 'Baṅ get giḍik',
'protect' => "banchao'",
'protect_change' => 'Judạ',
'protectthispage' => 'Noa sakam ban̕caome',
'youhavenewmessages' => 'Amaḱ do $1 ($2) menaḱa',
'newmessageslink' => 'Nãwã sombat',
'newmessagesdifflink' => 'Mucạt bodol',
-'youhavenewmessagesmulti' => 'Amaḱ nãwã mesag kodo',
+'youhavenewmessagesmulti' => 'Amaḱ nãwã mesagko do $1 menaḱa',
'editsection' => 'So̠mpado̠n',
'editold' => 'So̠mpado̠n',
'viewsourceold' => 'Ńamoḱ jayga',
'hidetoc' => 'uku, Danaṅ',
'collapsible-collapse' => 'Murchạo caba',
'collapsible-expand' => 'Phaylao',
-'thisisdeleted' => 'N̕el se doho ruạṛ',
-'viewdeleted' => 'Ńelme',
+'thisisdeleted' => 'Ńel se nãwã aroe hoyuka?',
+'viewdeleted' => '$1 Ńelme',
'feedlinks' => 'Jom oco',
+'site-rss-feed' => 'RSS jom oco',
'site-atom-feed' => ' $1 Jom oco',
'page-atom-feed' => '"$1" khon khudri jom',
'red-link-title' => '$1 (niạ sakamdo bạnuḱa)',
Judi noa do karon bań hoylen khan, noa do am sopṭoyer re kạtictem ńam daṛeyaḱa.
Daya katet́ noa do nonde [[Special:ListUsers/sysop|administrator]], ṭhen lạime, URL hotete.',
'missingarticle-rev' => '(Nãwã aro#: $1)',
-'missingarticle-diff' => 'pharak',
+'missingarticle-diff' => '(Pharak: $1, $2)',
'internalerror' => 'Bhitri reaḱ bhul',
'internalerror_info' => 'Bhitri reaḱ bhul:',
'filedeleteerror' => '1 sakam do baṅ get́ giḍiḱ lena',
'ns-specialprotected' => 'Asokay teaḱ sakamkodo baṅ oltoṅgea.',
# Virus scanner
-'virus-scanfailed' => 'Skan do baṇ hoylena',
+'virus-scanfailed' => 'Esken baṅ hoelena (Code $1)',
'virus-unknownscanner' => 'Baṅ urum anṭvayras:',
# Login and logout pages
+'welcomecreation' => '==Johar,==
+Amaḱ ekaunṭ do tearena.
+Amaḱ [Asokaete:Pasindko {{SITENAME}} pasindko]] bodol alom hiṛińa.',
'yourname' => 'Beoboharicaḱ ńutum',
'yourpassword' => 'Uku namber',
'yourpasswordagain' => 'Arhõ oku namber olme',
'logout' => 'Bahre oḍoń',
'userlogout' => 'Bahre oḍoń',
'notloggedin' => 'Bhitri baṅ bolokana',
-'nologin' => 'Ekaunṭ do menaḱgea?',
+'nologin' => 'Ekaunṭ bạnuḱtama?',
'nologinlink' => 'account tear me',
'createaccount' => 'Ṭhai benaome',
'gotaccount' => 'Ekaunṭ menaḱgeya?',
'noemailcreate' => 'Am do mitṭen jewet e-mail ṭhikạna em jaruṛ menaḱtama.',
'passwordsent' => '"$1" ṭhikạnate resṭariyen e-mail lạgit́te mitṭen oku nambar em hoyena.
Daya kate ńam porte arhõ bhitri boloḱme.',
-'mailerror' => 'Vul mail em:',
+'mailerror' => 'Vulte kulakan mail:',
'emailconfirmlink' => 'Amaḱ e-mail ṭhikana do sạriyme.',
'cannotchangeemail' => 'Ekaunṭ e-mail ṭhikạnakodo noa wiki re baṅ bodoloḱ kana.',
'accountcreated' => 'Ekaunṭ do teyarena',
or <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]</span>.',
'previewnote' => "'''Disạyme noa do eken ńeln̕am lạgit.'''
Amaḱ bodolaḱ kodo nit habićte bań ban̕cao akana!",
+'continue-editing' => 'Toṅge calaḱkana',
'editing' => 'Sampadon; joṛao',
'creating' => 'Benao',
'editingsection' => 'Joṛao $1 (hạṭiń)',
Thoṛa format do noare banuḱana.',
'post-expand-template-inclusion-category' => 'Sakamko oka borḍre noa tahẽna ona doe paromkeda',
+# Account creation failure
+'cantcreateaccounttitle' => 'Ekaunṭ do baṅ tearlena',
+
# History pages
'viewpagelogs' => 'Noa sakam reaḱ cạbi udukme',
'currentrev-asof' => 'Mucạt nãwã aroy',
'history-show-deleted' => 'khạli get giḍiyaḱ koge',
'histfirst' => "adi laha-ak'",
'histlast' => 'Nahak',
+'historyempty' => '(banuḱa)',
# Revision feed
'history-feed-title' => 'Jạṛ nãwã aroy',
'history-feed-item-nocomment' => 're',
# Revision deletion
+'rev-deleted-user' => '(laṛcaṛić ńutum ocoḱena)',
'rev-delundel' => 'ńeloḱ/danaṅ',
+'rev-showdeleted' => 'Uduḱme',
+'revisiondelete' => 'Get giḍi/nãwã aro baṅ getgiḍi',
+'revdelete-show-file-submit' => 'Hẽ',
+'revdelete-radio-same' => '(alom bodola)',
+'revdelete-radio-set' => 'Hẽ',
'revdel-restore' => 'Judạ lekate ńel',
'revdel-restore-deleted' => 'giḍikaḱ ńel ruạṛ',
'revdel-restore-visible' => 'Ńeloḱ leka paṛhao ruạṛ',
+'pagehist' => 'Sakam reaḱ jạṛ',
+'deletedhist' => 'Get giḍi jạṛ',
+'revdelete-reasonotherlist' => 'Eṭaḱak karon',
# Merge log
'revertmerge' => 'bań mit́',
'searchprofile-images-tooltip' => 'File sendra',
'searchprofile-everything-tooltip' => 'Sanam ko modre sẽndra ( roṛ sakam modre hõ)',
'searchprofile-advanced-tooltip' => 'Judạ ńutum re sẽndra',
-'search-result-size' => 'bạyiṭ aema bạyiṭ',
+'search-result-size' => 'Katha Kathako',
'search-redirect' => '($1 te sujhi doṛha )',
'search-section' => '(Pahaṭa $1)',
'search-suggest' => 'Am do cet́ $1 em menocoyet tãhẽkana',
'searchall' => 'Sanam',
'showingresultsheader' => "'''$4''' lạgit́ {{PLURAL:$5|Pho̠l ńamoḱ́akan - '''$1''' of '''$3'''|Pho̠l ńamoḱ́akan '''$1 - $2''' of '''$3'''}}",
'search-nonefound' => 'Kupuli leka roṛruạṛ bạnuḱa',
+'powersearch-togglelabel' => 'Sendra',
+'powersearch-toggleall' => 'Sanamaḱ',
+'powersearch-togglenone' => 'Okaṭaḱ hõ baṅ',
+
+# Quickbar
+'qbsettings-none' => 'Okaṭaḱ hõ baṅ',
# Preferences page
+'preferences' => 'Pạsindko',
'mypreferences' => 'Iñaḱ pạsindko',
+'changepassword' => 'Uku nombor bodolme',
+'prefs-skin' => 'Harta',
+'skin-preview' => 'Ńel, Unuduḱ',
+'datedefault' => 'Pạsind banuḱa',
+'prefs-resetpass' => 'Uku nombor bodolme',
+'prefs-changeemail' => 'E-mail ṭhikạna bodolme',
+'prefs-setemail' => 'E-mail ṭhikana benaome',
+'saveprefs' => 'Rukhiyạymẽ',
+'resetprefs' => 'Baṅ rukhiyạaḱ ocogmẽ',
+'rows' => 'Sạrko:',
+'searchresultshead' => 'Sendra',
+'timezoneregion-africa' => 'Aphrika',
+'timezoneregion-america' => 'Amirika',
+'timezoneregion-asia' => 'Esiya',
+'prefs-files' => 'Rẽtko',
'youremail' => 'E-mail:',
+'username' => 'Beoharićaḱ ńutum:',
+'uid' => 'Beoharićaḱ cinhạo nombor',
'yourrealname' => 'Sạ̣ri ńutum',
+'gender-male' => 'Baba hoṛ',
+'gender-female' => 'Gogo hoṛ, Kuṛi, Kuṛi gidrạ',
+'email' => 'E-mail',
'prefs-help-email' => 'E-mail ṭhikana do bạṛtitege, menkhan uku namber nãwãte benao jạruṛa, am do amaḱ uku nomborem hiṛiń keda.',
'prefs-help-email-others' => 'Am são e-mail hotete jogajog dohoy lạgitte mitṭen joṛao se amaḱ katha roṛaḱ sakam bachao jońme.
Amaḱ e-mail ṭhikạna do bań cabaḱa tinre onko do ko beohara',
+# User preference: e-mail validation using jQuery
+'email-address-validity-valid' => 'E-mail ṭhikạna do jewetge ńamena',
+'email-address-validity-invalid' => 'Amaḱ jewet e-mail ṭhkạna emmẽ',
+
+# User rights
+'userrights' => 'Beoharićaḱ laṛcaṛ ektiạrko',
+'userrights-lookup-user' => 'Beoharkoaḱ gãotako laṛcaṛ',
+'userrights-user-editname' => 'Beoharićaḱ ńutum emmẽ',
+'editusergroup' => 'Beoharićaḱ gãotako toṅgeymẽ',
+'userrights-editusergroup' => 'Beoharićaḱ gãotako toṅgeymẽ',
+'saveusergroups' => 'Beoharićaḱ gãotako rukhiyaymẽ',
+
+# Rights
+'right-edit' => 'Sakamko toṅge',
+'right-createpage' => 'Sakamko benoamẽ (Okako do galmarao sakamko baṅkan)',
+'right-createtalk' => 'Galmarao sakamko benaomẽ',
+'right-createaccount' => 'Nãwã beoharićaḱ ekaunṭ tearmẽ',
+'right-move' => 'Sakamko ocogmẽ',
+'right-move-subpages' => 'Sakam saõte kạtic sakamko ocogmẽ',
+'right-movefile' => 'Rẽtko ocogmẽ',
+'right-upload' => 'Rẽtko rakabmẽ',
+'right-delete' => 'Sakamko get giḍiymẽ',
+
# Associated actions - in the sentence "You do not have permission to X"
'action-edit' => 'noa sakam joṛao',
# Upload
'upload' => 'Fael aploḍme',
+'uploadbtn' => 'Rẽt rakabmẽ',
'uploadlogpage' => 'Chạbi do uthạome',
+'filename' => 'Rẽt ńutum',
'filedesc' => 'Guṭ katha',
+'fileuploadsummary' => 'Guṭ katha',
+'savefile' => 'Rẽt rukhiyaymẽ',
'uploadedimage' => 'Rakaṕ hoyena',
+'upload-description' => 'Rẽt reaḱ jạṛ',
+'watchthisupload' => 'Noa rẽt ńelmẽ',
+
+'upload-file-error' => 'Bhitri reaḱ bhul',
'license' => 'Laisence benao',
'license-header' => 'Laisense benao',
+# Special:ListFiles
+'imgfile' => 'Rẽt',
+'listfiles' => 'Rẽt reaḱ tạlika',
+'listfiles_date' => 'Tạrikh',
+'listfiles_name' => 'Ńutum',
+'listfiles_user' => 'Beoharić, Laṛcaṛic',
+
# File description page
'file-anchor-link' => 'Re̕t',
'filehist' => 'Fael renaḱ Jạṛ',
'filehist-help' => 'date re click me/somóy re click me fail reak obostha nel lagit',
+'filehist-deleteall' => 'Joto get giḍi',
+'filehist-deleteone' => 'Get giḍi',
'filehist-revert' => 'Lahaleka',
'filehist-current' => 'Nitaḱ',
'filehist-datetime' => '̣Tạrikh/So̠mo̠y',
# Statistics
'statistics' => 'Halot',
+'statistics-pages' => 'Sakamko',
'disambiguationspage' => 'sujhi',
'nbytes' => '$1 {{PLURAL:$1|baiṭ|baiṭ}}',
'nmembers' => 'Sãoten/ Sãotenko',
'prefixindex' => 'Sanam sakam re joṛao menaḱ',
+'shortpages' => 'Huḍiń sakamko',
+'longpages' => 'Jiliń sakamko',
+'listusers' => 'beoharićaḱ tạlika',
'usercreated' => 'Ayo baba: tạrikh okte',
'newpages' => 'Nãwa Patako',
+'newpages-username' => 'Beoharićaḱ ńutum:',
+'ancientpages' => 'Mare sakamko',
'move' => 'Ocoḱme, Kulme',
+'movethispage' => 'Noa sakam ocogmẽ',
# Book sources
'booksources' => 'Puthi ńamoḱ ṭhại/jayga',
'allpages' => 'joto sakam',
'alphaindexline' => '$1 hạbić $2',
'allarticles' => 'Sanam sakam',
+'allpagesprev' => 'Tayom sećaḱ',
+'allpagesnext' => 'Laha seć',
'allpagessubmit' => 'Calaḱme',
# Special:Categories
# Special:LinkSearch
'linksearch-line' => '$2 joṛao menaḱa $2re',
+# Special:ListUsers
+'listusers-submit' => 'Udugmẽ',
+'listusers-blocked' => '(Esetgea)',
+
# Special:Log/newusers
'newuserlogpage' => 'Laṛcaṛićaḱ tear cạbi',
# Special:ListGroupRights
+'listgrouprights-group' => 'Gaõta',
+'listgrouprights-rights' => 'Ạidạriko',
+'listgrouprights-helppage' => 'Goṛo:Gaõta ạidạri',
'listgrouprights-members' => 'Saõtenkoaḱ tạlika',
+'listgrouprights-addgroup-all' => 'Joto gaõtare ko soṅgekom',
+'listgrouprights-removegroup-all' => 'Joto gaõtaren ko ocoḱgiḍikom',
# E-mail user
'emailuser' => 'Nui beoharić e-mail emayme',
+'emailpage' => 'E-mail beoharić',
+'noemailtitle' => 'E-mail ṭhikạna do banuḱa',
+'emailusername' => 'Beoharićaḱ ńutum:',
+'emailusernamesubmit' => 'Em',
+'emailfrom' => 'Kulić:',
+'emailto' => 'Ńamić:',
+'emailmessage' => 'Mesag',
+'emailsend' => 'Kulmẽ',
# Watchlist
'watchlist' => "Inak' n'el ko",
'wlshowlast' => 'Mucạt 1 ghonta mucạt 2 maha uduḱme',
'watchlist-options' => 'Ńelok tạlika reak sonketko',
+# Displayed when you click the "watch" button and it is in the process of watching
+'watching' => 'Ńeloḱ kana...',
+
+'changed' => 'Bodolena',
+
# Delete
+'deletepage' => 'Sakam get giḍikam',
+'delete-legend' => 'Get giḍi',
'actioncomplete' => 'kami Chabae-ena',
'actionfailed' => 'Kami bang hoe-lena',
'dellogpage' => 'Mãrao log',
'protectcomment' => 'karon',
'protectexpiry' => 'Cabaḱ',
+# Restrictions (nouns)
+'restriction-edit' => 'Toṅge',
+'restriction-move' => 'Ocoḱmẽ, Kulmẽ',
+'restriction-create' => 'Tearmẽ, Benaomẽ',
+
# Undelete
'undeletelink' => 'Ńel/doho ruạṛ',
'undeleteviewlink' => 'Ńel',
'whatlinkshere-filters' => 'Sapha',
# Block/unblock
+'block' => 'Beoharić esedem',
+'blockip' => 'Beoharić esedem',
+'blockip-title' => 'Beoharić esedem',
+'blockip-legend' => 'Beoharić esedem',
'ipboptions' => '2 Ghonṭa : 2 hours, 1 maha:1 day, 3 maha : 3 days,1 hapta :1 week, 2 hapta : 2 weeks, 1 cando :1 month, 3 cando : 3 months,6 cando :6 months, 1 serma :1 year, Aemamaha : infinite',
'ipblocklist' => 'Beoharic esetgeyay',
+'ipblocklist-submit' => 'Sendra',
+'emailblock' => 'E-mail do esetgea',
'blocklink' => 'Eset',
'unblocklink' => 'bań block',
'change-blocklink' => 'block judạ',
'contribslink' => 'em daṛeaḱ',
+'emaillink' => 'E-mail kulmẽ',
'blocklogpage' => 'Tala eset',
'blocklogentry' => 'Eset [[$1]] sãote cabaḱ okte oka do $2 $3',
'block-log-flags-nocreate' => 'Ekaunṭ benao do bondogeya',
+'block-log-flags-noemail' => 'E-mail do esetgea',
+'block-log-flags-hiddenname' => 'Beoharićaḱ ńutum do ukugea',
+'blockme' => 'Esedińmẽ',
+'proxyblocksuccess' => 'Hoena',
# Move page
+'movepagebtn' => 'Sakam ocogmẽ, Sakam kulmẽ',
+'pagemovedsub' => 'Ocogoḱ do hoena',
'movelogpage' => 'Tala cạbi ocoḱme',
'revertmove' => 'ruạr agu',
# Export
'export' => 'Aguyen sakamko',
+'export-addcat' => 'Joṛaomẽ',
+'export-addns' => 'Joṛaomẽ',
# Namespace 8 related
'allmessagesname' => 'Ńutum',
'allmessagesdefault' => 'Bań bhul mesag ol',
+'allmessages-filter-all' => 'Sanamaḱ',
+'allmessages-filter-submit' => 'Calaḱmẽ',
# Thumbnails
'thumbnail-more' => 'Lạṭui mẽ',
+# Special:Import
+'import-upload-filename' => 'Rẽt ńutum',
+
# Tooltip help for the actions
'tooltip-pt-userpage' => 'Amak bebohar sakam',
'tooltip-pt-mytalk' => 'Amaḱ katha ro̠ṛrenaḱ́ pata',
'tooltip-ca-nstab-project' => "project page nel' me",
'tooltip-ca-nstab-image' => 'Fael sakam ńel',
'tooltip-ca-nstab-template' => 'Forom uduḱme',
+'tooltip-ca-nstab-help' => 'Goṛo sakam ńelmẽ',
'tooltip-ca-nstab-category' => 'Rokom sokom sakamko udukme',
'tooltip-minoredit' => 'Noa do huḍiń joṛao lekate lekhay me',
'tooltip-save' => 'Bodolaḱko rukhiyayme',
'tooltip-watch' => 'Amaḱ ńeloḱ sakamre noa do dohoyme',
'tooltip-rollback' => '"Ghurlạ ạcur" noa sakam taṛam ruạṛ ńel sapha ona do amaḱ mũcạt́ mit́ dhom click re',
'tooltip-undo' => 'Noa joṛao kạmire ulṭao "bạgiyaḱme" ar ńeloḱ lekate noa joṛao jhicme. Noa do am guḍ karon joṛaoe ektiyariye emama.',
+'tooltip-preferences-save' => 'Pạsindko rukhiyaymẽ',
'tooltip-summary' => 'Khaṭote guṭ katha bhoraome',
+# Attribution
+'others' => 'Eṭagaḱko',
+
# Info page
+'pageinfo-header-edits' => 'Toṅgeko',
'pageinfo-header-watchlist' => 'Ńeloḱ tạlika',
'pageinfo-header-views' => 'Ńelme',
'pageinfo-subjectpage' => 'Sakam',
'file-nohires' => 'Aema resulation nondḍe banuḱa',
'show-big-image' => 'Purạo resulation',
+# Special:NewFiles
+'ilsubmit' => 'Sendra',
+
# Bad image list
'bad_image_list' => 'Format do latar re leka',
isospeeddratings
jeleń',
+# EXIF tags
+'exif-imagewidth' => 'Ganḍe',
+'exif-imagelength' => 'Usul',
+'exif-datetime' => 'Rẽt bodol reaḱ tạrikh ar okte',
+'exif-artist' => 'Onoliạ',
+
# External editor support
'edit-externally' => 'Noa rẽt tońge joṛao lạ̣gitte bahre reaḱ koejoń beoharme',
'edit-externally-help' => '(Nonḍe ńelme [//www.mediawiki.org/wiki/Manual:External_editors setup instructions] bạṛtite baḍay lạgit)',
'filereadonlyerror' => 'Nebolo možné modifikovať súbor „$1“, pretože úložisko „$2“ je momentálne v režime len na čítanie.
Správca, ktorý ho zamkol ponúkol toto vysvetlenie: „$3“.',
+'invalidtitle-knownnamespace' => 'Neplatný názov s menným priestorom „$2“ a textom „$3“',
+'invalidtitle-unknownnamespace' => 'Neplatný názov s neznámym číslom menného priestoru „$1“ a textom „$2“',
# Virus scanner
'virus-badscanner' => "Chybná konfigurácia: neznámy antivírus: ''$1''",
'node-count-exceeded-warning' => 'Stránka prekročila povolený počet uzlov',
'expansion-depth-exceeded-category' => 'Stránky s priveľkou hĺbkou expanzie',
'expansion-depth-exceeded-warning' => 'Stránka prekročila povolenú hĺbku expanzie',
+'parser-unstrip-loop-warning' => 'Zistené zacyklenie volania rozširovacej značky',
+'parser-unstrip-recursion-limit' => 'Prektočený limit rekurzie volania rozširovacej značky ($1)',
# "Undo" feature
'undo-success' => 'Úpravu je možné vrátiť. Prosím skontrolujte tento rozdiel, čím overíte, že táto úprava je tá, ktorú chcete, a následne uložte zmeny, čím ukončíte vrátenie.',
# Diffs
'history-title' => 'História revízií „$1“',
+'difference-title' => 'Rozdiel medzi revíziami „$1“',
+'difference-title-multipage' => 'Rozdiel medzi stránkami „$1“ a „$2“',
'difference-multipage' => '(Rozdiel medzi stránkami)',
'lineno' => 'Riadok $1:',
'compareselectedversions' => 'Porovnať označené verzie',
'http-curl-error' => 'Chyba pri sťahovaní URL: $1',
'http-host-unreachable' => 'URL nie je dostupný',
'http-bad-status' => 'Počas požiadavky HTTP nastal problém: $1 $2',
+'http-truncated-body' => 'Telo požiadavky bolo prijaté iba čiastočne.',
# Some likely curl errors. More could be added from <http://curl.haxx.se/libcurl/c/libcurl-errors.html>
'upload-curl-error6' => 'Nedostupný URL',
'vector.css' => '/* Tu umiestnené CSS bude ovplyvňovať používateľov štýlu Vector */',
'print.css' => '/* Tu umiestnené CSS bude ovplyvňovať tlačový výstup */',
'handheld.css' => '/* Tu umiestnené CSS bude ovplyvňovať prenosné zariadenia vychádzajúceho zo štýlu nastaveného v $wgHandheldStyle */',
+'noscript.css' => '/* Tu umiestnené CSS bude ovplyvňovať používateľov s vypnutým JavaScriptom */',
+'group-autoconfirmed.css' => '/* Tu umiestnené CSS bude ovplyvňovať iba používateľov s overenou emailovou adresou */',
+'group-bot.css' => '/* Tu umiestnené CSS bude ovplyvňovať iba robotov */',
+'group-sysop.css' => '/* Tu umiestnené CSS bude ovplyvňovať iba správcov */',
+'group-bureaucrat.css' => '/* Tu umiestnené CSS bude ovplyvňovať iba byrokratov */',
# Scripts
'common.js' => '/* Tu uvedený JavaScript sa nahrá všetkým používateľom pri každom nahraní stránky. */',
'simple.js' => '/* Tu sa nachádzajúci JavaScript sa načíta používateľom vzhľadu Jednoduchý */',
'modern.js' => '/* Tu sa nachádzajúci JavaScript sa načíta používateľom vzhľadu Moderný */',
'vector.js' => '/* Tu sa nachádzajúci JavaScript sa načíta používateľom vzhľadu Vector */',
+'group-autoconfirmed.js' => '/* Tu sa nachádzajúci JavaScript sa načíta používateľom s potvrdenou emailovou adresou */',
+'group-bot.js' => '/* Tu sa nachádzajúci JavaScript sa načíta len robotom */',
+'group-sysop.js' => '/* Tu sa nachádzajúci JavaScript sa načíta len správcom */',
+'group-bureaucrat.js' => '/* Tu sa nachádzajúci JavaScript sa načíta len byrokratom */',
# Metadata
'notacceptable' => 'Wiki server nedokáže poskytovať dáta vo formáte, v akom ich váš klient vie čítať.',
'skinname-chick' => 'Kuriatko',
'skinname-simple' => 'Jednoduchý',
'skinname-modern' => 'Moderný',
+'skinname-vector' => 'Vector',
# Patrolling
'markaspatrolleddiff' => 'Označiť ako stráženú',
# Diffs
'history-title' => 'Lịch sử sửa đổi của “$1”',
+'difference-title' => 'Khác biệt giữa các bản “$1”',
+'difference-title-multipage' => 'Khác biệt giữa các trang “$1” và “$2”',
'difference-multipage' => '(Khác biệt giữa các trang)',
'lineno' => 'Dòng $1:',
'compareselectedversions' => 'So sánh các bản đã chọn',
'language-converter-depth-warning' => '字词转换器深度越限($1)',
'node-count-exceeded-category' => '页面的节点数超出限制',
'node-count-exceeded-warning' => '页面超出了节点数',
+'expansion-depth-exceeded-category' => '扩展深度超出限制的页面',
# "Undo" feature
'undo-success' => '此编辑可以被撤销。请检查以下比较以核实这正是您想做的,然后保存以下更改完成撤销编辑。',
'language-converter-depth-warning' => '已超過字詞轉換器深度限制($1)',
'node-count-exceeded-category' => '頁面的節點數超出限制',
'node-count-exceeded-warning' => '頁面超出節點數',
-'expansion-depth-exceeded-category' => '頁面的擴展深度超出限制',
+'expansion-depth-exceeded-category' => '擴展深度超出限制的頁面',
'expansion-depth-exceeded-warning' => '頁面超出擴展深度',
# "Undo" feature
--- /dev/null
+<?php
+/**
+ * Maintenance script to test fileop performance
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+
+$initialTime = microtime( true );
+$wgProfiler = array( 'class' => 'ProfilerSimpleText' );
+error_reporting( E_ALL );
+
+require_once( dirname( __FILE__ ) . '/Maintenance.php' );
+
+class TestFileOpPerformance extends Maintenance {
+ public function __construct() {
+ parent::__construct();
+ $this->mDescription = "Test fileop performance";
+ $this->addOption( 'b1', 'Backend 1', true, true );
+ $this->addOption( 'b2', 'Backend 2', false, true );
+ $this->addOption( 'srcdir', 'File source directory', true, true );
+ $this->addOption( 'maxfiles', 'Max files', false, true );
+ }
+
+ public function execute() {
+ $backend = FileBackendGroup::singleton()->get( $this->getOption( 'b1' ) );
+ $this->doPerfTest( $backend );
+
+ if ( $this->getOption( 'b2' ) ) {
+ $backend = FileBackendGroup::singleton()->get( $this->getOption( 'b2' ) );
+ $this->doPerfTest( $backend );
+ }
+
+ $profiler = Profiler::instance();
+ $profiler->setTemplated( true );
+ $profiler->logData(); // prints
+ }
+
+ protected function doPerfTest( FileBackend $backend ) {
+ $ops1 = array();
+ $ops2 = array();
+ $ops3 = array();
+ $ops4 = array();
+ $ops5 = array();
+
+ $baseDir = 'mwstore://' . $backend->getName() . '/testing-cont1';
+ $backend->prepare( array( 'dir' => $baseDir ) );
+
+ $dirname = $this->getOption( 'srcdir' );
+ $dir = opendir( $dirname );
+ if ( !$dir ) {
+ return;
+ }
+
+ while ( $dir && ( $file = readdir( $dir ) ) !== false ) {
+ if ( $file[0] != '.' ) {
+ $this->output( "Using '$dirname/$file' in operations.\n" );
+ $dst = $baseDir . '/' . wfBaseName( $file );
+ $ops1[] = array( 'op' => 'store', 'src' => "$dirname/$file", 'dst' => $dst, 'overwrite' => 1);
+ $ops2[] = array( 'op' => 'copy', 'src' => "$dst", 'dst' => "$dst-1", 'overwrite' => 1);
+ $ops3[] = array( 'op' => 'move', 'src' => $dst, 'dst' => "$dst-2", 'overwrite' => 1);
+ $ops4[] = array( 'op' => 'delete', 'src' => "$dst-1", 'overwrite' => 1 );
+ $ops5[] = array( 'op' => 'delete', 'src' => "$dst-2", 'overwrite' => 1 );
+ }
+ if ( count( $ops1 ) >= $this->getOption( 'maxfiles', 20 ) ) {
+ break; // enough
+ }
+ }
+ closedir( $dir );
+ $this->output( "\n" );
+
+ $start = microtime( true );
+ $status = $backend->doOperations( $ops1, array( 'force' => 1 ) );
+ $e = ( microtime( true ) - $start ) * 1000;
+ if ( $status->getErrorsArray() ) {
+ print_r( $status->getErrorsArray() );
+ exit(0);
+ }
+ $this->output( $backend->getName() . ": Stored " . count( $ops1 ) . " files in $e ms.\n" );
+
+ $start = microtime( true );
+ $backend->doOperations( $ops2, array( 'force' => 1 ) );
+ $e = ( microtime( true ) - $start ) * 1000;
+ if ( $status->getErrorsArray() ) {
+ print_r( $status->getErrorsArray() );
+ exit(0);
+ }
+ $this->output( $backend->getName() . ": Copied " . count( $ops2 ) . " files in $e ms.\n" );
+
+ $start = microtime( true );
+ $backend->doOperations( $ops3, array( 'force' => 1 ) );
+ $e = ( microtime( true ) - $start ) * 1000;
+ if ( $status->getErrorsArray() ) {
+ print_r( $status->getErrorsArray() );
+ exit(0);
+ }
+ $this->output( $backend->getName() . ": Moved " . count( $ops3 ) . " files in $e ms.\n" );
+
+ $start = microtime( true );
+ $backend->doOperations( $ops4, array( 'force' => 1 ) );
+ $e = ( microtime( true ) - $start ) * 1000;
+ if ( $status->getErrorsArray() ) {
+ print_r( $status->getErrorsArray() );
+ exit(0);
+ }
+ $this->output( $backend->getName() . ": Deleted " . count( $ops4 ) . " files in $e ms.\n" );
+
+ $start = microtime( true );
+ $backend->doOperations( $ops5, array( 'force' => 1 ) );
+ $e = ( microtime( true ) - $start ) * 1000;
+ if ( $status->getErrorsArray() ) {
+ print_r( $status->getErrorsArray() );
+ exit(0);
+ }
+ $this->output( $backend->getName() . ": Deleted " . count( $ops5 ) . " files in $e ms.\n" );
+ }
+}
+
+$maintClass = "TestFileOpPerformance";
+require_once( RUN_MAINTENANCE_IF_MAIN );
wfDeprecated( $function );
wfRestoreWarnings();
}
+
+ /**
+ * Asserts that the given database query yields the rows given by $expectedRows.
+ * The expected rows should be given as indexed (not associative) arrays, with
+ * the values given in the order of the columns in the $fields parameter.
+ * Note that the rows are sorted by the columns given in $fields.
+ *
+ * @param $table String|Array the table(s) to query
+ * @param $fields String|Array the columns to include in the result (and to sort by)
+ * @param $condition String|Array "where" condition(s)
+ * @param $expectedRows Array - an array of arrays giving the expected rows.
+ *
+ * @throws MWException if this test cases's needsDB() method doesn't return true.
+ * Test cases can use "@group Database" to enable database test support,
+ * or list the tables under testing in $this->tablesUsed, or override the
+ * needsDB() method.
+ */
+ protected function assertSelect( $table, $fields, $condition, Array $expectedRows ) {
+ if ( !$this->needsDB() ) {
+ throw new MWException( 'When testing database state, the test cases\'s needDB()' .
+ ' method should return true. Use @group Database or $this->tablesUsed.');
+ }
+
+ $db = wfGetDB( DB_SLAVE );
+
+ $res = $db->select( $table, $fields, $condition, array( 'ORDER BY' => $fields ) );
+ $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
+
+ $i = 0;
+
+ foreach ( $expectedRows as $expected ) {
+ $r = $res->fetchRow();
+ self::stripStringKeys( $r );
+
+ $i += 1;
+ $this->assertNotEmpty( $r, "row #$i missing" );
+
+ $this->assertEquals( $expected, $r, "row #$i mismatches" );
+ }
+
+ $r = $res->fetchRow();
+ self::stripStringKeys( $r );
+
+ $this->assertFalse( $r, "found extra row (after #$i)" );
+ }
+
+ /**
+ * Utility function for eliminating all string keys from an array.
+ * Useful to turn a database result row as returned by fetchRow() into
+ * a pure indexed array.
+ *
+ * @static
+ * @param $r mixed the array to remove string keys from.
+ */
+ protected static function stripStringKeys( &$r ) {
+ if ( !is_array( $r ) ) return;
+
+ foreach ( $r as $k => $v ) {
+ if ( is_string( $k ) ) unset( $r[$k] );
+ }
+ }
}
--- /dev/null
+<?php
+
+/**
+ *
+ * @group Database
+ * ^--- make sure temporary tables are used.
+ */
+class LinksUpdateTest extends MediaWikiTestCase {
+
+ function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed = array_merge ( $this->tablesUsed,
+ array( 'interwiki',
+
+ 'page_props',
+ 'pagelinks',
+ 'categorylinks',
+ 'langlinks',
+ 'externallinks',
+ 'imagelinks',
+ 'templatelinks',
+ 'iwlinks' ) );
+ }
+
+ function setUp() {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace( 'interwiki',
+ array('iw_prefix'),
+ array( 'iw_prefix' => 'linksupdatetest',
+ 'iw_url' => 'http://testing.com/wiki/$1',
+ 'iw_api' => 'http://testing.com/w/api.php',
+ 'iw_local' => 0,
+ 'iw_trans' => 0,
+ 'iw_wikiid' => 'linksupdatetest',
+ ) );
+ }
+
+ protected function makeTitleAndParserOutput( $name, $id ) {
+ $t = Title::newFromText( $name );
+ $t->mArticleID = $id; # XXX: this is fugly
+
+ $po = new ParserOutput();
+ $po->setTitleText( $t->getPrefixedText() );
+
+ return array( $t, $po );
+ }
+
+ public function testUpdate_pagelinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addLink( Title::newFromText( "Foo" ) );
+ $po->addLink( Title::newFromText( "Special:Foo" ) ); // special namespace should be ignored
+ $po->addLink( Title::newFromText( "linksupdatetest:Foo" ) ); // interwiki link should be ignored
+ $po->addLink( Title::newFromText( "#Foo" ) ); // hash link should be ignored
+
+ $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array(
+ array( NS_MAIN, 'Foo' ),
+ ) );
+
+ $po = new ParserOutput();
+ $po->setTitleText( $t->getPrefixedText() );
+
+ $po->addLink( Title::newFromText( "Bar" ) );
+
+ $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array(
+ array( NS_MAIN, 'Bar' ),
+ ) );
+ }
+
+ public function testUpdate_externallinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addExternalLink( "http://testing.com/wiki/Foo" );
+
+ $this->assertLinksUpdate( $t, $po, 'externallinks', 'el_to, el_index', 'el_from = 111', array(
+ array( 'http://testing.com/wiki/Foo', 'http://com.testing./wiki/Foo' ),
+ ) );
+ }
+
+ public function testUpdate_categorylinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addCategory( "Foo", "FOO" );
+
+ $this->assertLinksUpdate( $t, $po, 'categorylinks', 'cl_to, cl_sortkey', 'cl_from = 111', array(
+ array( 'Foo', "FOO\nTESTING" ),
+ ) );
+ }
+
+ public function testUpdate_iwlinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $target = Title::makeTitleSafe( NS_MAIN, "Foo", '', 'linksupdatetest' );
+ $po->addInterwikiLink( $target );
+
+ $this->assertLinksUpdate( $t, $po, 'iwlinks', 'iwl_prefix, iwl_title', 'iwl_from = 111', array(
+ array( 'linksupdatetest', 'Foo' ),
+ ) );
+ }
+
+ public function testUpdate_templatelinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addTemplate( Title::newFromText( "Template:Foo" ), 23, 42 );
+
+ $this->assertLinksUpdate( $t, $po, 'templatelinks', 'tl_namespace, tl_title', 'tl_from = 111', array(
+ array( NS_TEMPLATE, 'Foo' ),
+ ) );
+ }
+
+ public function testUpdate_imagelinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addImage( "Foo.png" );
+
+
+ $this->assertLinksUpdate( $t, $po, 'imagelinks', 'il_to', 'il_from = 111', array(
+ array( 'Foo.png' ),
+ ) );
+ }
+
+ public function testUpdate_langlinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addLanguageLink( Title::newFromText( "en:Foo" ) );
+
+
+ $this->assertLinksUpdate( $t, $po, 'langlinks', 'll_lang, ll_title', 'll_from = 111', array(
+ array( 'En', 'Foo' ),
+ ) );
+ }
+
+ public function testUpdate_page_props() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->setProperty( "foo", "bar" );
+
+ $this->assertLinksUpdate( $t, $po, 'page_props', 'pp_propname, pp_value', 'pp_page = 111', array(
+ array( 'foo', 'bar' ),
+ ) );
+ }
+
+ #@todo: test recursive, too!
+
+ protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, Array $expectedRows ) {
+ $update = new LinksUpdate( $title, $parserOutput );
+
+ $update->doUpdate();
+
+ $this->assertSelect( $table, $fields, $condition, $expectedRows );
+ }
+}
+
$this->singleBackend = new FSFileBackend( array(
'name' => 'localtesting',
'lockManager' => 'fsLockManager',
+ #'parallelize' => 'implicit',
'containerPaths' => array(
'unittest-cont1' => "{$tmpPrefix}-localtesting-cont1",
'unittest-cont2' => "{$tmpPrefix}-localtesting-cont2" )
$this->multiBackend = new FileBackendMultiWrite( array(
'name' => 'localtesting',
'lockManager' => 'fsLockManager',
+ 'parallelize' => 'implicit',
'backends' => array(
array(
'name' => 'localmutlitesting1',
$status = $this->backend->doOperation( $op );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Store from $source to $dest succeeded without warnings ($backendName)." );
- $this->assertEquals( array(), $status->errors,
+ $this->assertEquals( true, $status->isOK(),
"Store from $source to $dest succeeded ($backendName)." );
$this->assertEquals( array( 0 => true ), $status->success,
"Store from $source to $dest has proper 'success' field in Status ($backendName)." );
$status = $this->backend->doOperation(
array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of file at $source succeeded ($backendName)." );
if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
$status = $this->backend->doOperation( $op );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Copy from $source to $dest succeeded without warnings ($backendName)." );
$this->assertEquals( true, $status->isOK(),
"Copy from $source to $dest succeeded ($backendName)." );
$status = $this->backend->doOperation(
array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of file at $source succeeded ($backendName)." );
if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
}
$status = $this->backend->doOperation( $op );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Move from $source to $dest succeeded without warnings ($backendName)." );
$this->assertEquals( true, $status->isOK(),
"Move from $source to $dest succeeded ($backendName)." );
if ( $withSource ) {
$status = $this->backend->doOperation(
array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of file at $source succeeded ($backendName)." );
}
$status = $this->backend->doOperation( $op );
if ( $okStatus ) {
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Deletion of file at $source succeeded without warnings ($backendName)." );
$this->assertEquals( true, $status->isOK(),
"Deletion of file at $source succeeded ($backendName)." );
if ( $alreadyExists ) {
$status = $this->backend->doOperation(
array( 'op' => 'create', 'content' => $oldText, 'dst' => $dest ) );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of file at $dest succeeded ($backendName)." );
}
$status = $this->backend->doOperation( $op );
if ( $okStatus ) {
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of file at $dest succeeded without warnings ($backendName)." );
$this->assertEquals( true, $status->isOK(),
"Creation of file at $dest succeeded ($backendName)." );
}
$status = $this->backend->doOperations( $ops );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of source files succeeded ($backendName)." );
$dest = $params['dst'];
// Combine the files into one
$status = $this->backend->concatenate( $params );
if ( $okStatus ) {
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of concat file at $dest succeeded without warnings ($backendName)." );
$this->assertEquals( true, $status->isOK(),
"Creation of concat file at $dest succeeded ($backendName)." );
if ( $alreadyExists ) {
$this->prepare( array( 'dir' => dirname( $path ) ) );
$status = $this->backend->create( array( 'dst' => $path, 'content' => $content ) );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of file at $path succeeded ($backendName)." );
$size = $this->backend->getFileSize( array( 'src' => $path ) );
$status = $this->backend->doOperation(
array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of file at $source succeeded ($backendName)." );
$this->assertEquals( true, $status->isOK(),
"Creation of file at $source succeeded with OK status ($backendName)." );
$status = $this->backend->doOperation(
array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of file at $source succeeded ($backendName)." );
$tmpFile = $this->backend->getLocalCopy( array( 'src' => $source ) );
$status = $this->backend->doOperation(
array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of file at $source succeeded ($backendName)." );
$tmpFile = $this->backend->getLocalReference( array( 'src' => $source ) );
$status = $this->prepare( array( 'dir' => dirname( $path ) ) );
if ( $isOK ) {
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Preparing dir $path succeeded without warnings ($backendName)." );
$this->assertEquals( true, $status->isOK(),
"Preparing dir $path succeeded ($backendName)." );
$status = $this->backend->clean( array( 'dir' => dirname( $path ) ) );
if ( $isOK ) {
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Cleaning dir $path succeeded without warnings ($backendName)." );
$this->assertEquals( true, $status->isOK(),
"Cleaning dir $path succeeded ($backendName)." );
);
foreach ( $dirs as $dir ) {
$status = $this->prepare( array( 'dir' => $dir ) );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Preparing dir $dir succeeded without warnings ($backendName)." );
}
$status = $this->backend->clean(
array( 'dir' => "$base/unittest-cont1", 'recursive' => 1 ) );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Recursive cleaning of dir $dir succeeded without warnings ($backendName)." );
foreach ( $dirs as $dir ) {
$this->backend = $this->singleBackend;
$this->tearDownFiles();
- $this->doTestDoOperationsFailing();
+ $this->doTestDoOperations2();
$this->tearDownFiles();
$this->backend = $this->multiBackend;
$this->tearDownFiles();
+ $this->doTestDoOperations2();
+ $this->tearDownFiles();
+
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
$this->doTestDoOperationsFailing();
$this->tearDownFiles();
- // @TODO: test some cases where the ops should fail
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperationsFailing();
+ $this->tearDownFiles();
}
private function doTestDoOperations() {
// Does nothing
) );
- $this->assertEquals( array(), $status->errors, "Operation batch succeeded" );
+ $this->assertGoodStatus( $status, "Operation batch succeeded" );
$this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
$this->assertEquals( 13, count( $status->success ),
"Operation batch has correct success array" );
"Correct file SHA-1 of $fileC" );
}
- private function doTestDoOperationsFailing() {
+ // concurrency orientated
+ function doTestDoOperations2() {
+ $base = $this->baseStorePath();
+
+ $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
+ $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
+ $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
+
+ $tmpNameA = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ file_put_contents( $tmpNameA, $fileAContents );
+ $tmpNameB = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ file_put_contents( $tmpNameB, $fileBContents );
+ $tmpNameC = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ file_put_contents( $tmpNameC, $fileCContents );
+
+ $this->filesToPrune[] = $tmpNameA; # avoid file leaking
+ $this->filesToPrune[] = $tmpNameB; # avoid file leaking
+ $this->filesToPrune[] = $tmpNameC; # avoid file leaking
+
+ $fileA = "$base/unittest-cont1/a/b/fileA.txt";
+ $fileB = "$base/unittest-cont1/a/b/fileB.txt";
+ $fileC = "$base/unittest-cont1/a/b/fileC.txt";
+ $fileD = "$base/unittest-cont1/a/b/fileD.txt";
+
+ $this->prepare( array( 'dir' => dirname( $fileA ) ) );
+ $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileB ) ) );
+ $this->prepare( array( 'dir' => dirname( $fileC ) ) );
+ $this->prepare( array( 'dir' => dirname( $fileD ) ) );
+
+ $status = $this->backend->doOperations( array(
+ array( 'op' => 'store', 'src' => $tmpNameA, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ array( 'op' => 'store', 'src' => $tmpNameB, 'dst' => $fileB, 'overwrite' => 1 ),
+ array( 'op' => 'store', 'src' => $tmpNameC, 'dst' => $fileC, 'overwrite' => 1 ),
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<empty>, D:<A>
+ array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ),
+ // Now: A:<A>, B:<empty>, C:<B>, D:<A>
+ array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ),
+ // Now: A:<B>, B:<empty>, C:<empty>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ),
+ // Now: A:<B>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Now: A:<empty>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Does nothing
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Does nothing
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Does nothing
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Does nothing
+ array( 'op' => 'null' ),
+ // Does nothing
+ ) );
+
+ $this->assertGoodStatus( $status, "Operation batch succeeded" );
+ $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
+ $this->assertEquals( 16, count( $status->success ),
+ "Operation batch has correct success array" );
+
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ),
+ "File does not exist at $fileA" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ),
+ "File does not exist at $fileB" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ),
+ "File does not exist at $fileD" );
+
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ),
+ "File exists at $fileC" );
+ $this->assertEquals( $fileBContents,
+ $this->backend->getFileContents( array( 'src' => $fileC ) ),
+ "Correct file contents of $fileC" );
+ $this->assertEquals( strlen( $fileBContents ),
+ $this->backend->getFileSize( array( 'src' => $fileC ) ),
+ "Correct file size of $fileC" );
+ $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
+ $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ),
+ "Correct file SHA-1 of $fileC" );
+ }
+
+ function doTestDoOperationsFailing() {
$base = $this->baseStorePath();
$fileA = "$base/unittest-cont2/a/b/fileA.txt";
$ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
}
$status = $this->backend->doOperations( $ops );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of files succeeded ($backendName)." );
$this->assertEquals( true, $status->isOK(),
"Creation of files succeeded with OK status ($backendName)." );
$ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
}
$status = $this->backend->doOperations( $ops );
- $this->assertEquals( array(), $status->errors,
+ $this->assertGoodStatus( $status,
"Creation of files succeeded ($backendName)." );
$this->assertEquals( true, $status->isOK(),
"Creation of files succeeded with OK status ($backendName)." );
}
private function recursiveClean( $dir ) {
- do {
- if ( !$this->backend->clean( array( 'dir' => $dir ) )->isOK() ) {
- break;
- }
- } while ( $dir = FileBackend::parentStoragePath( $dir ) );
+ $this->backend->clean( array( 'dir' => $dir, 'recursive' => 1 ) );
+ }
+
+ function assertGoodStatus( $status, $msg ) {
+ $this->assertEquals( print_r( array(), 1 ), print_r( $status->errors, 1 ), $msg );
}
function tearDown() {
QUnit.config.urlConfig.push( 'mwlogenv' );
/**
- * Reset mw.config to a fresh copy of the live config for each test();
- * @param override {Object} [optional]
- * @example:
- * <code>
- * module( .., newMwEnvironment() );
- *
- * test( .., function () {
- * mw.config.set( 'foo', 'bar' ); // just for this test
- * } );
- *
- * test( .., function () {
- * mw.config.get( 'foo' ); // doesn't exist
- * } );
- *
- *
- * module( .., newMwEnvironment({ quux: 'corge' }) );
- *
- * test( .., function () {
- * mw.config.get( 'quux' ); // "corge"
- * mw.config.set( 'quux', "grault" );
- * } );
- *
- * test( .., function () {
- * mw.config.get( 'quux' ); // "corge"
- * } );
+ * Reset mw.config and others to a fresh copy of the live config for each test(),
+ * and restore it back to the live one afterwards.
+ * @param localEnv {Object} [optional]
+ * @example (see test suite at the bottom of this file)
* </code>
*/
QUnit.newMwEnvironment = ( function () {
- var log, liveConfig, liveMsgs;
+ var log, liveConfig, liveMessages;
liveConfig = mw.config.values;
- liveMsgs = mw.messages.values;
+ liveMessages = mw.messages.values;
function freshConfigCopy( custom ) {
// "deep=true" is important here.
// e.g. mw.config.set( 'wgFileExtensions', [] ) would not effect liveConfig,
// but mw.config.get( 'wgFileExtensions' ).push( 'png' ) would as the array
// was passed by reference in $.extend's loop.
- return $.extend({}, liveConfig, custom, /*deep=*/true );
+ return $.extend( {}, liveConfig, custom, /*deep=*/true );
}
- function freshMsgsCopy( custom ) {
- return $.extend({}, liveMsgs, custom, /*deep=*/true );
+ function freshMessagesCopy( custom ) {
+ return $.extend( {}, liveMessages, custom, /*deep=*/true );
}
log = QUnit.urlParams.mwlogenv ? mw.log : function () {};
- return function ( overrideConfig, overrideMsgs ) {
- overrideConfig = overrideConfig || {};
- overrideMsgs = overrideMsgs || {};
+ return function ( localEnv ) {
+ localEnv = $.extend( {
+ // QUnit
+ setup: $.noop,
+ teardown: $.noop,
+ // MediaWiki
+ config: {},
+ messages: {}
+ }, localEnv );
return {
setup: function () {
log( 'MwEnvironment> SETUP for "' + QUnit.config.current.module
+ ': ' + QUnit.config.current.testName + '"' );
+
// Greetings, mock environment!
- mw.config.values = freshConfigCopy( overrideConfig );
- mw.messages.values = freshMsgsCopy( overrideMsgs );
+ mw.config.values = freshConfigCopy( localEnv.config );
+ mw.messages.values = freshMessagesCopy( localEnv.messages );
+
+ localEnv.setup()
},
teardown: function () {
log( 'MwEnvironment> TEARDOWN for "' + QUnit.config.current.module
+ ': ' + QUnit.config.current.testName + '"' );
+
+ localEnv.teardown();
+
// Farewell, mock environment!
mw.config.values = liveConfig;
- mw.messages.values = liveMsgs;
+ mw.messages.values = liveMessages;
}
};
};
$.extend( QUnit, addons );
$.extend( window, addons );
+/**
+ * Small test suite to confirm proper functionality of the utilities and
+ * initializations in this file.
+ */
+var envExecCount = 0;
+module( 'mediawiki.tests.qunit.testrunner', QUnit.newMwEnvironment({
+ setup: function () {
+ envExecCount += 1;
+ this.mwHtmlLive = mw.html;
+ mw.html = {
+ escape: function () {
+ return 'mocked-' + envExecCount;
+ }
+ };
+ },
+ teardown: function () {
+ mw.html = this.mwHtmlLive;
+ },
+ config: {
+ testVar: 'foo'
+ },
+ messages: {
+ testMsg: 'Foo.'
+ }
+}) );
+
+test( 'Setup', function () {
+ expect( 3 );
+
+ equal( mw.html.escape( 'foo' ), 'mocked-1', 'extra setup() callback was ran.' );
+ equal( mw.config.get( 'testVar' ), 'foo', 'config object applied' );
+ equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object applied' );
+
+ mw.config.set( 'testVar', 'bar' );
+ mw.messages.set( 'testMsg', 'Bar.' );
+});
+
+test( 'Teardown', function () {
+ expect( 3 );
+
+ equal( mw.html.escape( 'foo' ), 'mocked-2', 'extra setup() callback was re-ran.' );
+ equal( mw.config.get( 'testVar' ), 'foo', 'config object restored and re-applied after test()' );
+ equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object restored and re-applied after test()' );
+});
+
+module( 'mediawiki.tests.qunit.testrunner-after', QUnit.newMwEnvironment() );
+
+test( 'Teardown', function () {
+ expect( 3 );
+
+ equal( mw.html.escape( '<' ), '<', 'extra teardown() callback was ran.' );
+ equal( mw.config.get( 'testVar' ), null, 'config object restored to live in next module()' );
+ equal( mw.messages.get( 'testMsg' ), null, 'messages object restored to live in next module()' );
+});
+
})( jQuery, mediaWiki, QUnit );
wgContentLanguage: 'en'
};
-module( 'jquery.tablesorter', QUnit.newMwEnvironment( config ) );
+module( 'jquery.tablesorter', QUnit.newMwEnvironment({ config: config }) );
test( '-- Initial check', function() {
expect(1);
"wgCaseSensitiveNamespaces": []
};
-module( 'mediawiki.Title', QUnit.newMwEnvironment( config ) );
+module( 'mediawiki.Title', QUnit.newMwEnvironment({ config: config }) );
test( '-- Initial check', function () {
expect(1);
test( '-- Initial check', function () {
expect( 2 );
- // Ensure we have a generic URI parser if not running in a browser
- if ( !mw.Uri ) {
- mw.Uri = mw.UriRelative( 'http://example.com/' );
- }
+ // Ensure we have a generic mw.Uri constructor. By default mediawiki.uri,
+ // will use the currrent window ocation as base. But for testing we need
+ // to have a generic one, so that it doens't return false negatives if
+ // we run the test suite from an https server.
+ mw.Uri = mw.UriRelative( 'http://example.org/w/index.php' );
ok( mw.UriRelative, 'mw.UriRelative defined' );
ok( mw.Uri, 'mw.Uri defined' );