wikimedia/mediawiki-extensions-Translate

View on GitHub
scripts/poimport.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php
/**
 * Imports gettext files exported from Special:Translate back.
 *
 * @author Niklas Laxström
 * @author Siebrand Mazeland
 * @copyright Copyright © 2007-2013 Niklas Laxström, Siebrand Mazeland
 * @license GPL-2.0-or-later
 * @file
 */

use MediaWiki\CommentStore\CommentStoreComment;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
use MediaWiki\Extension\Translate\MessageLoading\MessageCollection;
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Title\Title;
use MediaWiki\User\User;

// Standard boilerplate to define $IP
if ( getenv( 'MW_INSTALL_PATH' ) !== false ) {
    $IP = getenv( 'MW_INSTALL_PATH' );
} else {
    $dir = __DIR__;
    $IP = "$dir/../../..";
}
require_once "$IP/maintenance/Maintenance.php";

class Poimport extends Maintenance {
    public function __construct() {
        parent::__construct();
        $this->addDescription( 'Po file importer (does not make changes unless specified).' );
        $this->addOption(
            'file',
            'Gettext file to import (Translate specific formatting)',
            true, /*required*/
            true /*has arg*/
        );
        $this->addOption(
            'user',
            'User who makes edits to wiki',
            true, /*required*/
            true /*has arg*/
        );
        $this->addOption(
            'really',
            '(optional) Actually make changes',
            false, /*required*/
            false /*has arg*/
        );
        $this->requireExtension( 'Translate' );
    }

    public function execute() {
        // Parse the po file.
        $p = new PoImporter( $this->getOption( 'file' ) );
        $p->setProgressCallback( [ $this, 'myOutput' ] );
        [ $changes, $group ] = $p->parse();

        if ( !count( $changes ) ) {
            $this->output( "No changes to import\n" );
            exit( 0 );
        }

        // Import changes to wiki.
        $w = new WikiWriter(
            $changes,
            $group,
            $this->getOption( 'user' ),
            !$this->hasOption( 'really' )
        );

        $w->setProgressCallback( [ $this, 'myOutput' ] );
        $w->execute();
    }

    /**
     * Public alternative for protected Maintenance::output() as we need to get
     * messages from the ChangeSyncer class to the commandline.
     * @param string $text The text to show to the user
     * @param string|null $channel Unique identifier for the channel.
     * @param bool $error Whether this is an error message
     */
    public function myOutput( $text, $channel = null, $error = false ) {
        if ( $error ) {
            $this->error( $text );
        } else {
            $this->output( $text, $channel );
        }
    }
}

/**
 * Parses a po file that has been exported from MediaWiki. Other files are not
 * supported.
 */
class PoImporter {
    /** @var callable Function to report progress updates */
    private $progressCallback;
    /**
     * Path to file to parse.
     * @var string
     */
    private $file;

    /** @param string $file File to import */
    public function __construct( $file ) {
        $this->file = $file;
    }

    public function setProgressCallback( $callback ) {
        $this->progressCallback = $callback;
    }

    /// @see Maintenance::output for param docs
    protected function reportProgress( $text, $channel = null, $severity = 'status' ) {
        if ( is_callable( $this->progressCallback ) ) {
            $useErrorOutput = $severity === 'error';
            call_user_func( $this->progressCallback, $text, $channel, $useErrorOutput );
        }
    }

    /**
     * Loads translations for comparison.
     *
     * @param string $id Id of MessageGroup.
     * @param string $code Language code.
     * @return MessageCollection
     */
    protected function initMessages( $id, $code ) {
        $group = MessageGroups::getGroup( $id );

        $messages = $group->initCollection( $code );
        $messages->loadTranslations();

        return $messages;
    }

    /**
     * Parses relevant stuff from the po file.
     * @return array|bool
     */
    public function parse() {
        $data = file_get_contents( $this->file );
        $data = TextContent::normalizeLineEndings( $data );

        $matches = [];
        if ( preg_match( '/X-Language-Code:\s+(.*)\\\n/', $data, $matches ) ) {
            $code = $matches[1];
            $this->reportProgress( "Detected language as $code", 'code' );
        } else {
            $this->reportProgress( 'Unable to determine language code', 'code', 'error' );

            return false;
        }

        if ( preg_match( '/X-Message-Group:\s+(.*)\\\n/', $data, $matches ) ) {
            $groupId = $matches[1];
            $this->reportProgress( "Detected message group as $groupId", 'group' );
        } else {
            $this->reportProgress( 'Unable to determine message group', 'group', 'error' );

            return false;
        }

        $contents = $this->initMessages( $groupId, $code );

        echo "----\n";

        $poformat = '".*"\n?(^".*"$\n?)*';
        $quotePattern = '/(^"|"$\n?)/m';

        $sections = preg_split( '/\n{2,}/', $data );
        $changes = [];
        foreach ( $sections as $section ) {
            $matches = [];
            if ( preg_match( "/^msgctxt\s($poformat)/mx", $section, $matches ) ) {
                // Remove quoting
                $key = preg_replace( $quotePattern, '', $matches[1] );

                // Ignore unknown keys
                if ( !isset( $contents[$key] ) ) {
                    continue;
                }
            } else {
                continue;
            }
            $matches = [];
            if ( preg_match( "/^msgstr\s($poformat)/mx", $section, $matches ) ) {
                // Remove quoting
                $translation = preg_replace( $quotePattern, '', $matches[1] );
                // Restore new lines and remove quoting
                $translation = stripcslashes( $translation );
            } else {
                continue;
            }

            // Fuzzy messages
            if ( preg_match( '/^#, fuzzy$/m', $section ) ) {
                $translation = TRANSLATE_FUZZY . $translation;
            }

            $oldtranslation = (string)$contents[$key]->translation();

            if ( $translation !== $oldtranslation ) {
                if ( $translation === '' ) {
                    $this->reportProgress( "Skipping empty translation in the po file for $key!\n" );
                } else {
                    if ( $oldtranslation === '' ) {
                        $this->reportProgress( "New translation for $key\n" );
                    } else {
                        $this->reportProgress( "Translation of $key differs:\n$translation\n" );
                    }
                    $changes["$key/$code"] = $translation;
                }
            }
        }

        return [ $changes, $groupId ];
    }
}

/**
 * Import changes to wiki as given user
 */
class WikiWriter {
    /** @var callable|null Function to report progress updates */
    private $progressCallback;
    /** @var User */
    private $user;
    /** @var string[] */
    private $changes;
    /** @var bool */
    private $dryrun;
    /** @var MessageGroup|null */
    private $group;

    /**
     * @param string[] $changes Array of key/langcode => translation.
     * @param string $groupId
     * @param string $user User who makes the edits in wiki.
     * @param bool $dryrun Do not do anything that affects the database.
     */
    public function __construct( array $changes, $groupId, $user, $dryrun = true ) {
        $this->changes = $changes;
        $this->group = MessageGroups::getGroup( $groupId );
        $this->user = MediaWikiServices::getInstance()
            ->getUserFactory()
            ->newFromName( $user );
        $this->dryrun = $dryrun;
    }

    public function setProgressCallback( $callback ) {
        $this->progressCallback = $callback;
    }

    /// @see Maintenance::output for param docs
    protected function reportProgress( $text, $channel, $severity = 'status' ) {
        if ( is_callable( $this->progressCallback ) ) {
            $useErrorOutput = $severity === 'error';
            call_user_func( $this->progressCallback, $text, $channel, $useErrorOutput );
        }
    }

    /**
     * Updates pages on by one.
     */
    public function execute() {
        if ( !$this->group ) {
            $this->reportProgress( 'Given group does not exist.', 'groupId', 'error' );

            return;
        }

        if ( !$this->user->idForName() ) {
            $this->reportProgress( 'Given user does not exist.', 'user', 'error' );

            return;
        }

        $count = count( $this->changes );
        $this->reportProgress( "Going to update $count pages.", 'pagecount' );

        $ns = $this->group->getNamespace();

        foreach ( $this->changes as $title => $text ) {
            $this->updateMessage( $ns, $title, $text );
        }
    }

    /**
     * Actually adds the new translation.
     * @param int $namespace
     * @param string $page
     * @param string $text
     */
    private function updateMessage( $namespace, $page, $text ) {
        $title = Title::makeTitleSafe( $namespace, $page );

        if ( !$title instanceof Title ) {
            $this->reportProgress( 'INVALID TITLE!', $page, 'error' );

            return;
        }
        $this->reportProgress( "Updating {$title->getPrefixedText()}... ", $title );

        if ( $this->dryrun ) {
            $this->reportProgress( 'DRY RUN!', $title );

            return;
        }

        $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
        $content = ContentHandler::makeContent( $text, $title );
        $updater = $page->newPageUpdater( $this->user )->setContent( SlotRecord::MAIN, $content );

        if ( $this->user->authorizeWrite( 'autopatrol', $title ) ) {
            $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
        }

        $summary = CommentStoreComment::newUnsavedComment( 'Updating translation from gettext import' );
        $updater->saveRevision( $summary );
        $status = $updater->getStatus();

        if ( $status->isOK() ) {
            $this->reportProgress( 'OK!', $title );
        } else {
            $this->reportProgress( 'Failed!', $title );
        }
    }
}

$maintClass = Poimport::class;
require_once RUN_MAINTENANCE_IF_MAIN;