r54722 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r54721‎ | r54722 | r54723 >
Date:14:34, 10 August 2009
Author:nikerabbit
Status:ok
Tags:
Comment:
Gettext web import should now be complete, but test carefully
Modified paths:
  • /trunk/extensions/Translate/SpecialImportTranslations.php (modified) (history)
  • /trunk/extensions/Translate/Translate.i18n.php (modified) (history)
  • /trunk/extensions/Translate/_autoload.php (modified) (history)
  • /trunk/extensions/Translate/utils/MessageWebImporter.php (added) (history)

Diff [purge]

Index: trunk/extensions/Translate/SpecialImportTranslations.php
@@ -48,14 +48,38 @@
4949 return;
5050 }
5151
52 - // Proceed to loading and parsing if possible
53 - $file = null;
54 - $msg = $this->loadFile( $file );
55 - if ( $this->checkError( $msg ) ) return;
 52+ if ( $this->request->getCheck( 'process' ) ) {
 53+ $data = $this->getCachedData();
 54+ if (!$data) {
 55+ $this->out->addWikiMsg( 'session_fail_preview' ); // Core... bad
 56+ $this->outputForm();
 57+ return;
 58+ }
5659
57 - $msg = $this->parseFile( $file );
58 - if ( $this->checkError( $msg ) ) return;
 60+ } else {
 61+ // Proceed to loading and parsing if possible
 62+ // TODO: use a Status object instead?
 63+ $file = null;
 64+ $msg = $this->loadFile( $file );
 65+ if ( $this->checkError( $msg ) ) return;
5966
 67+ $msg = $this->parseFile( $file );
 68+ if ( $this->checkError( $msg ) ) return;
 69+
 70+ $data = $msg[1];
 71+ $this->setCachedData( $data );
 72+ }
 73+
 74+ $messages = $data['MESSAGES'];
 75+ $group = $data['METADATA']['group'];
 76+ $code = $data['METADATA']['code'];
 77+
 78+ $importer = new MessageWebImporter( $this->getTitle(), $group, $code );
 79+ $alldone = $importer->execute( $messages );
 80+ if ( $alldone ) {
 81+ $this->deleteCachedData();
 82+ }
 83+
6084 }
6185
6286 /**
@@ -80,8 +104,8 @@
81105 protected function outputForm() {
82106 $this->out->addScriptClass( 'TranslateImport' );
83107
 108+ // Ugly but necessary form building ahead, ohoy
84109 $this->out->addHTML(
85 -
86110 Xml::openElement( 'form', array(
87111 'action' => $this->getTitle()->getLocalUrl(),
88112 'method' => 'post',
@@ -143,7 +167,6 @@
144168
145169 $url = $this->request->getText( 'upload-url' );
146170 $status = Http::doDownload( $url, false );
147 - var_dump( $status );
148171 if ( $status->isOk() ) {
149172 $filedata = $status->value;
150173 return array( 'ok' );
@@ -170,19 +193,62 @@
171194 }
172195 }
173196
 197+ /**
 198+ * Try parsing file.
 199+ */
174200 protected function parseFile( $data ) {
175 - $matches = array();
176 - if ( preg_match( '/X-Language-Code:\s+([a-zA-Z-_]+)/', $data, $matches ) ) {
177 - $code = $matches[1];
178 - } else {
179 - return array( 'no-language-code' );
 201+ // Construct a dummy group for us...
 202+ // Time to rethink the interface again?
 203+ $group = MessageGroupBase::factory(
 204+ array(
 205+ 'FILES' => array(
 206+ 'class' => 'GettextFFS',
 207+ 'CtxtAsKey' => true,
 208+ ),
 209+ 'BASIC' => array(
 210+ 'class' => 'FileBasedMessageGroup',
 211+ 'namespace' => -1,
 212+ )
 213+ )
 214+ );
 215+
 216+ $ffs = new GettextFFS( $group );
 217+ $data = $ffs->readFromVariable( $data );
 218+
 219+ // Special data added by GettextFFS
 220+ $metadata = $data['METADATA'];
 221+
 222+ // This should catch everything that is not a po file exported form us
 223+ if ( !isset($metadata['code']) || !isset($metadata['group']) ) {
 224+ return array( 'no-headers' );
180225 }
181226
182 - if ( preg_match( '/X-Message-Group:\s+([a-zA-Z0-9-._]+)/', $data, $matches ) ) {
183 - $groupId = $matches[1];
184 - } else {
185 - return array( 'no-group-id' );
 227+ // And check for stupid editors which like to drop msgctxt..
 228+ // which unfortunately breaks submission
 229+ if ( isset($metadata['warnings']) ) {
 230+ global $wgLang;
 231+ return array( 'warnings', $wgLang->commaList( $metadata['warnings'] ) );
186232 }
 233+
 234+ return array( 'ok', $data );
187235 }
188236
 237+ protected function setCachedData( $data ) {
 238+ global $wgMemc;
 239+ $key = wfMemcKey( 'translate', 'webimport', $this->user->getId() );
 240+ $wgMemc->set( $key, $data, 60*15 ); // 15 minutes
 241+ }
 242+
 243+ protected function getCachedData() {
 244+ global $wgMemc;
 245+ $key = wfMemcKey( 'translate', 'webimport', $this->user->getId() );
 246+ return $wgMemc->get( $key );
 247+ }
 248+
 249+ protected function deleteCachedData() {
 250+ global $wgMemc;
 251+ $key = wfMemcKey( 'translate', 'webimport', $this->user->getId() );
 252+ return $wgMemc->delete( $key );
 253+ }
 254+
189255 }
\ No newline at end of file
Index: trunk/extensions/Translate/_autoload.php
@@ -81,6 +81,7 @@
8282 $wgAutoloadClasses['JsSelectToInput'] = $dir . 'utils/JsSelectToInput.php';
8383 $wgAutoloadClasses['HTMLJsSelectToInputField'] = $dir . 'utils/HTMLJsSelectToInputField.php';
8484 $wgAutoloadClasses['MessageGroupCache'] = $dir . 'utils/MessageGroupCache.php';
 85+$wgAutoloadClasses['MessageWebImporter'] = $dir . 'utils/MessageWebImporter.php';
8586
8687
8788 # predefined groups
Index: trunk/extensions/Translate/utils/MessageWebImporter.php
@@ -0,0 +1,364 @@
 2+<?php
 3+/**
 4+ * Class which encapsulates message importing. It scans for changes (new, changed, deleted),
 5+ * displays them in pretty way with diffs and finally executes the actions the user choices.
 6+ *
 7+ * @addtogroup Extensions
 8+ *
 9+ * @author Niklas Laxström
 10+ * @copyright Copyright © 2009, Niklas Laxström
 11+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
 12+ */
 13+
 14+class MessageWebImporter {
 15+ protected $title;
 16+ protected $user;
 17+ protected $group;
 18+ protected $code;
 19+
 20+ protected $processingTime = 10; // Seconds
 21+
 22+ public function __construct( Title $title = null, $group = null, $code = 'en' ) {
 23+ $this->setTitle( $title );
 24+ $this->setGroup( $group );
 25+ $this->setCode( $code );
 26+ }
 27+
 28+
 29+ // Wrapper for consistency with SpecialPage
 30+ public function getTitle() { return $this->title; }
 31+ public function setTitle( Title $title ) { $this->title = $title; }
 32+
 33+
 34+ public function getUser() {
 35+ global $wgUser;
 36+ return $this->user ? $this->user : $wgUser;
 37+ }
 38+ public function setUser( User $user ) { $this->user = $user; }
 39+
 40+
 41+ public function getGroup() { return $this->group; }
 42+ /**
 43+ * Group is either MessageGroup object or group id.
 44+ */
 45+ public function setGroup( $group ) {
 46+ if ( $group instanceof MessageGroup ) {
 47+ $this->group = $group;
 48+ } else {
 49+ $this->group = MessageGroups::getGroup( $group );
 50+ }
 51+ }
 52+
 53+ public function getCode() { return $this->code; }
 54+ public function setCode( $code = 'en' ) { $this->code = $code; }
 55+
 56+
 57+ protected function getAction() {
 58+ return $this->getTitle()->getFullURL();
 59+ }
 60+
 61+ protected function doHeader() {
 62+ TranslateUtils::injectCSS();
 63+
 64+ $formParams = array(
 65+ 'method' => 'post',
 66+ 'action' => $this->getAction(),
 67+ 'class' => 'mw-translate-manage'
 68+ );
 69+
 70+ return
 71+ Xml::openElement( 'form', $formParams ) .
 72+ Xml::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
 73+ Xml::hidden( 'token', $this->getUser()->editToken() ) .
 74+ Xml::hidden( 'process', 1 )
 75+ ;
 76+ }
 77+
 78+ protected function doFooter() {
 79+ return '</form>';
 80+ }
 81+
 82+ protected function allowProcess() {
 83+ global $wgRequest;
 84+ if ( $wgRequest->wasPosted() &&
 85+ $wgRequest->getBool( 'process', false ) &&
 86+ $this->getUser()->matchEditToken( $wgRequest->getVal( 'token' ) ) ) {
 87+
 88+ return true;
 89+ }
 90+ return false;
 91+ }
 92+
 93+ protected function getActions() {
 94+ if ( $this->code === 'en' ) {
 95+ return array( 'import', 'fuzzy', 'ignore' );
 96+ } else {
 97+ return array( 'import', 'conflict', 'ignore' );
 98+ }
 99+ }
 100+
 101+ protected function getDefaultAction( $fuzzy, $action ) {
 102+ if ( $action ) return $action;
 103+ return $fuzzy ? 'conflict' : 'import';
 104+ }
 105+
 106+
 107+ public function execute( $messages ) {
 108+ global $wgOut;
 109+ $this->out = $wgOut;
 110+
 111+ // Set up diff engine
 112+ $diff = new DifferenceEngine;
 113+ $diff->showDiffStyle();
 114+ $diff->setReducedLineNumbers();
 115+
 116+ // Check whether we do processing
 117+ $process = $this->allowProcess();
 118+
 119+ // Initialise collection
 120+ $group = $this->getGroup();
 121+ $code = $this->getCode();
 122+ $collection = $group->initCollection( $code );
 123+ $collection->loadTranslations();
 124+
 125+ $this->out->addHTML( $this->doHeader() );
 126+
 127+ // Determine changes
 128+ $alldone = $process;
 129+ $changed = array();
 130+ foreach ( $messages as $key => $value ) {
 131+ $fuzzy = $old = false;
 132+ if ( isset($collection[$key]) ) {
 133+ $old = $collection[$key]->translation();
 134+ $fuzzy = TranslateEditAddons::hasFuzzyString( $old ) ||
 135+ TranslateEditAddons::isFuzzy( self::makeTitle( $group, $key, $code ) );
 136+ }
 137+
 138+ // No changes at all, ignore
 139+ if ( strval($old) === strval($value) ) continue;
 140+
 141+ if ( $old === false ) {
 142+ $name = wfMsgHtml( 'translate-manage-import-new',
 143+ '<code style="font-weight:normal;">' . htmlspecialchars($key) . '</code>'
 144+ );
 145+ $text = TranslateUtils::convertWhiteSpaceToHTML( $value );
 146+ $changed[] = $this->makeSectionElement( $name, 'new', $text );
 147+ } else {
 148+ $diff->setText( $old, $value );
 149+ $text = $diff->getDiff( '', '' );
 150+ $type = 'changed';
 151+
 152+ global $wgRequest;
 153+ $action = $wgRequest->getVal( "action-$type-$key" );
 154+
 155+ if ( $process ) {
 156+ if ( !count($changed) ) $changed[] = '<ul>';
 157+
 158+ global $wgLang;
 159+ if ( $action === null ) {
 160+ $message = wfMsgExt( 'translate-manage-inconsistent', 'parseinline', wfEscapeWikiText( "action-$type-$key" ) );
 161+ $changed[] = "<li>$message</li></ul>";
 162+ $process = false;
 163+ } else {
 164+ // Check processing time
 165+ if ( !isset($this->time) ) $this->time = wfTimestamp();
 166+
 167+ $message = $this->doAction( $action, $group, $key, $code, $value );
 168+
 169+ $key = array_shift( $message );
 170+ $params = $message;
 171+ $message = wfMsgExt( $key, 'parseinline', $params );
 172+ $changed[] = "<li>$message</li>";
 173+
 174+ if ( $this->checkProcessTime() ) {
 175+ $process = false;
 176+ $duration = $wgLang->formatNum( $this->processingTime );
 177+ $message = wfMsgExt( 'translate-manage-toolong', 'parseinline', $duration );
 178+ $changed[] = "<li>$message</li></ul>";
 179+ }
 180+ continue;
 181+ }
 182+ }
 183+
 184+ $alldone = false;
 185+
 186+ $actions = $this->getActions();
 187+ $defaction = $this->getDefaultAction( $fuzzy, $action );
 188+
 189+ $act = array();
 190+
 191+ foreach ( $actions as $action ) {
 192+ $label = wfMsg( "translate-manage-action-$action" );
 193+ $act[] = Xml::radioLabel( $label, "action-$type-$key", $action, "action-$key-$action", $action === $defaction );
 194+ }
 195+
 196+ $name = wfMsg( 'translate-manage-import-diff',
 197+ '<code style="font-weight:normal;">' . htmlspecialchars($key) . '</code>',
 198+ implode( ' ', $act )
 199+ );
 200+
 201+ $changed[] = $this->makeSectionElement( $name, $type, $text );
 202+ }
 203+ }
 204+
 205+
 206+ if ( !$process ) {
 207+ $collection->filter( 'hastranslation', false );
 208+ $keys = array_keys($collection->keys());
 209+
 210+ $diff = array_diff( $keys, array_keys($messages) );
 211+
 212+ foreach ( $diff as $s ) {
 213+ $name = wfMsgHtml( 'translate-manage-import-deleted',
 214+ '<code style="font-weight:normal;">' . htmlspecialchars($s) . '</code>'
 215+ );
 216+ $text = TranslateUtils::convertWhiteSpaceToHTML( $collection[$s]->translation() );
 217+ $changed[] = $this->makeSectionElement( $name, 'deleted', $text );
 218+ }
 219+ }
 220+
 221+ if ( $process || (!count($changed) && $code !== 'en') ) {
 222+ if ( !count($changed) ) $this->out->addWikiMsg( 'translate-manage-nochanges-other' );
 223+
 224+ if ( !count($changed) || strpos( $changed[count($changed)-1], '<li>' ) !== 0 ) $changed[] = '<ul>';
 225+
 226+ $message = wfMsgExt( 'translate-manage-import-done', 'parseinline' );
 227+ $changed[] = "<li>$message</li></ul>";
 228+ $this->out->addHTML( implode( "\n", $changed ) );
 229+ } else {
 230+
 231+ // END
 232+
 233+ if ( count($changed) ) {
 234+ if ( $code === 'en' ) {
 235+ $this->out->addWikiMsg( 'translate-manage-intro-en' );
 236+ } else {
 237+ global $wgLang;
 238+ $lang = TranslateUtils::getLanguageName( $code, false, $wgLang->getCode() );
 239+ $this->out->addWikiMsg( 'translate-manage-intro-other', $lang );
 240+ }
 241+ $this->out->addHTML( Xml::hidden( 'language', $code ) );
 242+ $this->out->addHTML( implode( "\n", $changed ) );
 243+ $this->out->addHTML( Xml::submitButton( wfMsg( 'translate-manage-submit' ) ) );
 244+ } else {
 245+ $this->out->addWikiMsg( 'translate-manage-nochanges' );
 246+ }
 247+ }
 248+
 249+ $this->out->addHTML( $this->doFooter() );
 250+ return $alldone;
 251+ }
 252+
 253+ protected function doAction( $action, $group, $key, $code, $message, $comment = '' ) {
 254+ if ( $action === 'import' || $action === 'conflict' ) {
 255+
 256+ if ( $action === 'import' ) {
 257+ $comment = wfMsgForContentNoTrans( 'translate-manage-import-summary' );
 258+ } else {
 259+ $comment = wfMsgForContentNoTrans( 'translate-manage-conflict-summary' );
 260+ $message = TRANSLATE_FUZZY . $message;
 261+ }
 262+
 263+ $title = self::makeTitle( $group, $key, $code );
 264+ return $this->doImport( $title, $message, $comment );
 265+ } elseif ( $action === 'ignore' ) {
 266+ return array( 'translate-manage-import-ignore', $key );
 267+ } elseif ( $action === 'fuzzy' ) {
 268+ return $this->doFuzzy( $title, $message, $comment );
 269+ } else {
 270+ throw new MWException( "Unhandled action $action" );
 271+ }
 272+ }
 273+
 274+ protected function checkProcessTime() {
 275+ return wfTimestamp() - $this->time >= $this->processingTime;
 276+ }
 277+
 278+ protected function doImport( $title, $message, $comment, $user = null ) {
 279+ $flags = EDIT_FORCE_BOT;
 280+ $article = new Article( $title );
 281+ $status = $article->doEdit( $message, $comment, $flags );
 282+ $success = $status->isOK();
 283+
 284+ if ( $success ) {
 285+ return array( 'translate-manage-import-ok',
 286+ wfEscapeWikiText( $title->getPrefixedText() )
 287+ );
 288+ } else {
 289+ throw new MWException( "Failed to import new version of page {$title->getPrefixedText()}\n{$status->getWikiText()}" );
 290+ }
 291+ }
 292+
 293+ protected function doFuzzy( $title, $message, $comment ) {
 294+ $dbw = wfGetDB( DB_MASTER );
 295+ $titleText = $title->getDBKey();
 296+ $condArray = array(
 297+ 'page_namespace' => $title->getNamespace(),
 298+ 'page_latest=rev_id',
 299+ 'rev_text_id=old_id',
 300+ "page_title LIKE '{$dbw->escapeLike( $titleText )}/%%'"
 301+ );
 302+
 303+ $rows = $dbr->select(
 304+ array( 'page', 'revision', 'text' ),
 305+ array( 'page_title', 'page_namespace', 'old_text', 'old_flags' ),
 306+ $conds,
 307+ __METHOD__
 308+ );
 309+
 310+ $changed = array();
 311+ $fuzzybot = self::getFuzzyBot();
 312+ foreach ( $rows as $row ) {
 313+ $ttitle = Title::makeTitle( $row->page_namespace, $row->page_title );
 314+
 315+ $changed[] = $this->doImport(
 316+ $ttitle,
 317+ TRANSLATE_FUZZY . Revision::getRevisionText( $row ),
 318+ $comment,
 319+ $fuzzybot
 320+ );
 321+
 322+ if ( $this->checkProcessTime() ) break;
 323+
 324+ }
 325+
 326+ if ( count($changed) === count($rows) ) {
 327+ $comment = wfMsgForContentNoTrans( 'translate-manage-import-summary' );
 328+ $changed[] = $this->doImport( $title, $message, $comment );
 329+ }
 330+
 331+ $text = '';
 332+ foreach ( $changed as $c ) {
 333+ $key = array_shift( $c );
 334+ $text = "* " . wfMsgExt( $key, array(), $c );
 335+ }
 336+
 337+ return array( 'translate-manage-import-fuzzy',
 338+ "\n" . $text
 339+ );
 340+ }
 341+
 342+ protected static function getFuzzyBot() {
 343+ global $wgTranslateFuzzyBotName;
 344+ $user = User::newFromName( $wgTranslateFuzzyBotName );
 345+ if ( !$user->isLoggedIn() ) $user->addToDatabase();
 346+ return $user;
 347+ }
 348+
 349+ protected static function makeTitle( $group, $key, $code ) {
 350+ $ns = $group->getNamespace();
 351+ $titlekey = "$key/$code";
 352+ return Title::makeTitleSafe( $ns, $titlekey );
 353+ }
 354+
 355+ protected function makeSectionElement( $legend, $type, $content ) {
 356+ $containerParams = array( 'class' => "mw-tpt-sp-section mw-tpt-sp-section-type-{$type}" );
 357+ $legendParams = array( 'class' => 'mw-translate-manage-legend' );
 358+ $contentParams = array( 'class' => 'mw-tpt-sp-content' );
 359+
 360+ return Xml::tags( 'div', $containerParams,
 361+ Xml::tags( 'div', $legendParams, $legend ) .
 362+ Xml::tags( 'div', $contentParams, $content )
 363+ );
 364+ }
 365+}
\ No newline at end of file
Property changes on: trunk/extensions/Translate/utils/MessageWebImporter.php
___________________________________________________________________
Name: svn:eol-style
1366 + native
Index: trunk/extensions/Translate/Translate.i18n.php
@@ -295,10 +295,11 @@
296296 'translate-import-err-invalid-title' => 'Provided file name <nowiki>$1</nowiki> was invalid.',
297297 'translate-import-err-no-such-file' => 'File <nowiki>$1</nowiki> does not exist or has not been uploaded locally.',
298298
299 - 'translate-import-err-no-language-code' => 'File is not a well formed Gettext file in Translate extension format:
300 -Unable to determine language code.',
301 - 'translate-import-err-no-language-code' => 'File is not a well formed Gettext file in Translate extension format:
302 -Unable to determine group ID.',
 299+ 'translate-import-err-no-headers' => 'File is not a well formed Gettext file in Translate extension format:
 300+Unable to determine group and language from file headers.',
 301+ 'translate-import-err-warnings' => 'The file is not well formed.
 302+Make sure your editor does not remove msgctxt fields.
 303+Details: $1',
303304 );
304305
305306 /** Message documentation (Message documentation)

Status & tagging log