TranslationHelpers.php

Go to the documentation of this file.
00001 <?php
00017 class TranslationHelpers {
00022     protected $handle;
00027     protected $group;
00028 
00032     protected $translation;
00036     protected $definition;
00041     protected $textareaId = 'wpTextbox1';
00045     protected $editMode = 'true';
00046 
00051     public function __construct( Title $title, $groupId ) {
00052         $this->handle = new MessageHandle( $title );
00053         $this->group = $this->getMessageGroup( $this->handle, $groupId );
00054     }
00055 
00065     protected function getMessageGroup( MessageHandle $handle, $groupId ) {
00066         $mg = MessageGroups::getGroup( $groupId );
00067 
00068         # If we were not given (a valid) group
00069         if ( $mg === null ) {
00070             $groupId = MessageIndex::getPrimaryGroupId( $handle );
00071             $mg = MessageGroups::getGroup( $groupId );
00072         }
00073 
00074         return $mg;
00075     }
00076 
00081     public function getTextareaId() {
00082         return $this->textareaId;
00083     }
00084 
00089     public function setTextareaId( $id ) {
00090         $this->textareaId = $id;
00091     }
00092 
00097     public function setEditMode( $mode = true ) {
00098         $this->editMode = $mode;
00099     }
00100 
00105     public function getDefinition() {
00106         if ( $this->definition !== null ) {
00107             return $this->definition;
00108         }
00109 
00110         $this->mustBeKnownMessage();
00111 
00112         if ( method_exists( $this->group, 'getMessageContent' ) ) {
00113             $this->definition = $this->group->getMessageContent( $this->handle );
00114         } else {
00115             $this->definition = $this->group->getMessage(
00116                 $this->handle->getKey(),
00117                 $this->group->getSourceLanguage()
00118             );
00119         }
00120 
00121         return $this->definition;
00122     }
00123 
00129     public function getTranslation() {
00130         if ( $this->translation === null ) {
00131             $obj = new CurrentTranslationAid( $this->group, $this->handle, RequestContext::getMain() );
00132             $aid = $obj->getData();
00133             $this->translation = $aid['value'];
00134 
00135             if ( $aid['fuzzy'] ) {
00136                 $this->translation = TRANSLATE_FUZZY . $this->translation;
00137             }
00138         }
00139 
00140         return $this->translation;
00141     }
00142 
00148     public function setTranslation( $translation ) {
00149         $this->translation = $translation;
00150     }
00151 
00155     public function getTargetLanguage() {
00156         global $wgLanguageCode, $wgTranslateDocumentationLanguageCode;
00157 
00158         $code = $this->handle->getCode();
00159         if ( !$code ) {
00160             $this->mustBeKnownMessage();
00161             $code = $this->group->getSourceLanguage();
00162         }
00163         if ( $code === $wgTranslateDocumentationLanguageCode ) {
00164             return $wgLanguageCode;
00165         }
00166 
00167         return $code;
00168     }
00169 
00177     public function getBoxes( $suggestions = 'sync' ) {
00178         // Box filter
00179         $all = $this->getBoxNames();
00180 
00181         if ( $suggestions === 'async' ) {
00182             $all['translation-memory'] = array( $this, 'getLazySuggestionBox' );
00183         } elseif ( $suggestions === 'only' ) {
00184             return (string)$this->callBox(
00185                 'translation-memory',
00186                 $all['translation-memory'],
00187                 array( 'lazy' )
00188             );
00189         } elseif ( $suggestions === 'checks' ) {
00190             $request = RequestContext::getMain()->getRequest();
00191             $this->translation = $request->getText( 'translation' );
00192 
00193             return (string)$this->callBox( 'check', $all['check'] );
00194         }
00195 
00196         if ( $this->group instanceof RecentMessageGroup ) {
00197             $all['last-diff'] = array( $this, 'getLastDiff' );
00198         }
00199 
00200         $boxes = array();
00201         foreach ( $all as $type => $cb ) {
00202             $box = $this->callBox( $type, $cb );
00203             if ( $box ) {
00204                 $boxes[$type] = $box;
00205             }
00206         }
00207 
00208         wfRunHooks( 'TranslateGetBoxes', array( $this->group, $this->handle, &$boxes ) );
00209 
00210         if ( count( $boxes ) ) {
00211             return Html::rawElement(
00212                 'div',
00213                 array( 'class' => 'mw-sp-translate-edit-fields' ),
00214                 implode( "\n\n", $boxes )
00215             );
00216         } else {
00217             return '';
00218         }
00219     }
00220 
00225     public function callBox( $type, $cb, $params = array() ) {
00226         try {
00227             return call_user_func_array( $cb, $params );
00228         } catch ( TranslationHelperException $e ) {
00229             return "<!-- Box $type not available: {$e->getMessage()} -->";
00230         }
00231     }
00232 
00236     public function getBoxNames() {
00237         return array(
00238             'other-languages' => array( $this, 'getOtherLanguagesBox' ),
00239             'translation-memory' => array( $this, 'getSuggestionBox' ),
00240             'translation-diff' => array( $this, 'getPageDiff' ),
00241             'separator' => array( $this, 'getSeparatorBox' ),
00242             'documentation' => array( $this, 'getDocumentationBox' ),
00243             'definition' => array( $this, 'getDefinitionBox' ),
00244             'check' => array( $this, 'getCheckBox' ),
00245         );
00246     }
00247 
00255     protected function getTTMServerBox( $serviceName, $config ) {
00256         $this->mustHaveDefinition();
00257         $this->mustBeTranslation();
00258 
00259         $source = $this->group->getSourceLanguage();
00260         $code = $this->handle->getCode();
00261         $definition = $this->getDefinition();
00262         $TTMServer = TTMServer::factory( $config );
00263         $suggestions = $TTMServer->query( $source, $code, $definition );
00264         if ( count( $suggestions ) === 0 ) {
00265             throw new TranslationHelperException( 'No suggestions' );
00266         }
00267 
00268         return $suggestions;
00269     }
00270 
00278     protected function getRemoteTTMServerBox( $serviceName, $config ) {
00279         $this->mustHaveDefinition();
00280         $this->mustBeTranslation();
00281 
00282         self::checkTranslationServiceFailure( $serviceName );
00283 
00284         $source = $this->group->getSourceLanguage();
00285         $code = $this->handle->getCode();
00286         $definition = $this->getDefinition();
00287         $params = array(
00288             'format' => 'json',
00289             'action' => 'ttmserver',
00290             'sourcelanguage' => $source,
00291             'targetlanguage' => $code,
00292             'text' => $definition,
00293             '*', // Because we hate IE
00294         );
00295 
00296         wfProfileIn( 'TranslateWebServiceRequest-' . $serviceName );
00297         $json = Http::get( wfAppendQuery( $config['url'], $params ) );
00298         wfProfileOut( 'TranslateWebServiceRequest-' . $serviceName );
00299 
00300         $response = FormatJson::decode( $json, true );
00301 
00302         if ( $json === false ) {
00303             // Most likely a timeout or other general error
00304             self::reportTranslationServiceFailure( $serviceName );
00305             throw new TranslationHelperException( 'No reply from remote server' );
00306         } elseif ( !is_array( $response ) ) {
00307             error_log( __METHOD__ . ': Unable to parse reply: ' . strval( $json ) );
00308             throw new TranslationHelperException( 'Malformed reply from remote server' );
00309         }
00310 
00311         if ( !isset( $response['ttmserver'] ) ) {
00312             self::reportTranslationServiceFailure( $serviceName );
00313             throw new TranslationHelperException( 'Unexpected reply from remote server' );
00314         }
00315 
00316         $suggestions = $response['ttmserver'];
00317         if ( count( $suggestions ) === 0 ) {
00318             throw new TranslationHelperException( 'No suggestions' );
00319         }
00320 
00321         return $suggestions;
00322     }
00323 
00325     protected function formatTTMServerSuggestions( $data ) {
00326         $sugFields = array();
00327 
00328         foreach ( $data as $serviceWrapper ) {
00329             $config = $serviceWrapper['config'];
00330             $suggestions = $serviceWrapper['suggestions'];
00331 
00332             foreach ( $suggestions as $s ) {
00333                 $tooltip = wfMessage( 'translate-edit-tmmatch-source', $s['source'] )->plain();
00334                 $text = wfMessage(
00335                     'translate-edit-tmmatch',
00336                     sprintf( '%.2f', $s['quality'] * 100 )
00337                 )->plain();
00338                 $accuracy = Html::element( 'span', array( 'title' => $tooltip ), $text );
00339                 $legend = array( $accuracy => array() );
00340 
00341                 $TTMServer = TTMServer::factory( $config );
00342                 if ( $TTMServer->isLocalSuggestion( $s ) ) {
00343                     $title = Title::newFromText( $s['location'] );
00344                     $symbol = isset( $config['symbol'] ) ? $config['symbol'] : '•';
00345                     $legend[$accuracy][] = self::ajaxEditLink( $title, $symbol );
00346                 } else {
00347                     if ( $TTMServer instanceof RemoteTTMServer ) {
00348                         $displayName = $config['displayname'];
00349                     } else {
00350                         $wiki = WikiMap::getWiki( $s['wiki'] );
00351                         $displayName = $wiki->getDisplayName() . ': ' . $s['location'];
00352                     }
00353 
00354                     $params = array(
00355                         'href' => $TTMServer->expandLocation( $s ),
00356                         'target' => '_blank',
00357                         'title' => $displayName,
00358                     );
00359 
00360                     $symbol = isset( $config['symbol'] ) ? $config['symbol'] : '‣';
00361                     $legend[$accuracy][] = Html::element( 'a', $params, $symbol );
00362                 }
00363 
00364                 $suggestion = $s['target'];
00365                 $text = $this->suggestionField( $suggestion );
00366                 $params = array( 'class' => 'mw-sp-translate-edit-tmsug' );
00367 
00368                 // Group identical suggestions together
00369                 if ( isset( $sugFields[$suggestion] ) ) {
00370                     $sugFields[$suggestion][2] = array_merge_recursive( $sugFields[$suggestion][2], $legend );
00371                 } else {
00372                     $sugFields[$suggestion] = array( $text, $params, $legend );
00373                 }
00374             }
00375         }
00376 
00377         $boxes = array();
00378         foreach ( $sugFields as $field ) {
00379             list( $text, $params, $label ) = $field;
00380             $legend = array();
00381 
00382             foreach ( $label as $acc => $links ) {
00383                 $legend[] = $acc . ' ' . implode( " ", $links );
00384             }
00385 
00386             $legend = implode( ' | ', $legend );
00387             $boxes[] = Html::rawElement(
00388                 'div',
00389                 $params,
00390                 self::legend( $legend ) . $text . self::clear()
00391             ) . "\n";
00392         }
00393 
00394         // Limit to three best
00395         $boxes = array_slice( $boxes, 0, 3 );
00396         $result = implode( "\n", $boxes );
00397 
00398         return $result;
00399     }
00400 
00405     public function getSuggestionBox() {
00406         global $wgTranslateTranslationServices;
00407 
00408         $handlers = array(
00409             'microsoft' => 'getMicrosoftSuggestion',
00410             'apertium' => 'getApertiumSuggestion',
00411             'yandex' => 'getYandexSuggestion',
00412         );
00413 
00414         $errors = '';
00415         $boxes = array();
00416         $TTMSSug = array();
00417         foreach ( $wgTranslateTranslationServices as $name => $config ) {
00418             $type = $config['type'];
00419 
00420             if ( !isset( $config['timeout'] ) ) {
00421                 $config['timeout'] = 3;
00422             }
00423 
00424             $method = null;
00425             if ( isset( $handlers[$type] ) ) {
00426                 $method = $handlers[$type];
00427 
00428                 try {
00429                     $boxes[] = $this->$method( $name, $config );
00430                 } catch ( TranslationHelperException $e ) {
00431                     $errors .= "<!-- Box $name not available: {$e->getMessage()} -->\n";
00432                 }
00433                 continue;
00434             }
00435 
00436             $server = TTMServer::factory( $config );
00437             if ( $server instanceof RemoteTTMServer ) {
00438                 $method = 'getRemoteTTMServerBox';
00439             } elseif ( $server instanceof ReadableTTMServer ) {
00440                 $method = 'getTTMServerBox';
00441             }
00442 
00443             if ( !$method ) {
00444                 throw new MWException( __METHOD__ . ": Unsupported type {$config['type']}" );
00445             }
00446 
00447             try {
00448                 $TTMSSug[$name] = array(
00449                     'config' => $config,
00450                     'suggestions' => $this->$method( $name, $config ),
00451                 );
00452             } catch ( TranslationHelperException $e ) {
00453                 $errors .= "<!-- Box $name not available: {$e->getMessage()} -->\n";
00454             }
00455         }
00456 
00457         if ( count( $TTMSSug ) ) {
00458             array_unshift( $boxes, $this->formatTTMServerSuggestions( $TTMSSug ) );
00459         }
00460 
00461         // Remove nulls and falses
00462         $boxes = array_filter( $boxes );
00463 
00464         // Enclose if there is more than one box
00465         if ( count( $boxes ) ) {
00466             $sep = Html::element( 'hr', array( 'class' => 'mw-translate-sep' ) );
00467 
00468             return $errors . TranslateUtils::fieldset(
00469                 wfMessage( 'translate-edit-tmsugs' )->escaped(),
00470                 implode( "$sep\n", $boxes ),
00471                 array( 'class' => 'mw-translate-edit-tmsugs' )
00472             );
00473         } else {
00474             return $errors;
00475         }
00476     }
00477 
00478     protected static function makeGoogleQueryParams( $definition, $pair, $config ) {
00479         global $wgSitename, $wgVersion, $wgProxyKey;
00480 
00481         $app = "$wgSitename (MediaWiki $wgVersion; Translate " . TRANSLATE_VERSION . ")";
00482         $context = RequestContext::getMain();
00483         $options = array();
00484         $options['timeout'] = $config['timeout'];
00485 
00486         $options['postData'] = array(
00487             'q' => $definition,
00488             'v' => '1.0',
00489             'langpair' => $pair,
00490             // Unique but not identifiable
00491             'userip' => sha1( $wgProxyKey . $context->getUser()->getName() ),
00492             'x-application' => $app,
00493         );
00494 
00495         if ( $config['key'] ) {
00496             $options['postData']['key'] = $config['key'];
00497         }
00498 
00499         return $options;
00500     }
00501 
00502     protected function getMicrosoftSuggestion( $serviceName, $config ) {
00503         $this->mustHaveDefinition();
00504         self::checkTranslationServiceFailure( $serviceName );
00505 
00506         $code = $this->handle->getCode();
00507         $definition = trim( strval( $this->getDefinition() ) );
00508         $definition = self::wrapUntranslatable( $definition );
00509 
00510         $memckey = wfMemckey( 'translate-tmsug-badcodes-' . $serviceName );
00511         $unsupported = wfGetCache( CACHE_ANYTHING )->get( $memckey );
00512 
00513         if ( isset( $unsupported[$code] ) ) {
00514             throw new TranslationHelperException( 'Unsupported language' );
00515         }
00516 
00517         $options = array();
00518         $options['timeout'] = $config['timeout'];
00519 
00520         $params = array(
00521             'text' => $definition,
00522             'to' => $code,
00523         );
00524 
00525         if ( isset( $config['key'] ) ) {
00526             $params['appId'] = $config['key'];
00527         } else {
00528             throw new TranslationHelperException( 'API key is not set' );
00529         }
00530 
00531         $url = $config['url'] . '?' . wfArrayToCgi( $params );
00532         $url = wfExpandUrl( $url );
00533 
00534         $options['method'] = 'GET';
00535 
00536         $req = MWHttpRequest::factory( $url, $options );
00537 
00538         wfProfileIn( 'TranslateWebServiceRequest-' . $serviceName );
00539         $status = $req->execute();
00540         wfProfileOut( 'TranslateWebServiceRequest-' . $serviceName );
00541 
00542         if ( !$status->isOK() ) {
00543             $error = $req->getContent();
00544             if ( strpos( $error, 'must be a valid language' ) !== false ) {
00545                 $unsupported[$code] = true;
00546                 wfGetCache( CACHE_ANYTHING )->set( $memckey, $unsupported, 60 * 60 * 8 );
00547                 throw new TranslationHelperException( 'Unsupported language code' );
00548             }
00549 
00550             if ( $error ) {
00551                 error_log( __METHOD__ . ': Http::get failed:' . $error );
00552             } else {
00553                 error_log( __METHOD__ . ': Unknown error, grr' );
00554             }
00555             // Most likely a timeout or other general error
00556             self::reportTranslationServiceFailure( $serviceName );
00557         }
00558 
00559         $ret = $req->getContent();
00560         $text = preg_replace( '~<string.*>(.*)</string>~', '\\1', $ret );
00561         $text = Sanitizer::decodeCharReferences( $text );
00562         $text = self::unwrapUntranslatable( $text );
00563         $text = $this->suggestionField( $text );
00564 
00565         return Html::rawElement( 'div', array(), self::legend( $serviceName ) . $text . self::clear() );
00566     }
00567 
00568     protected static function wrapUntranslatable( $text ) {
00569         $text = str_replace( "\n", "!N!", $text );
00570         $wrap = '<span class="notranslate">\0</span>';
00571         $pattern = '~%[^% ]+%|\$\d|{VAR:[^}]+}|{?{(PLURAL|GRAMMAR|GENDER):[^|]+\||%(\d\$)?[sd]~';
00572         $text = preg_replace( $pattern, $wrap, $text );
00573 
00574         return $text;
00575     }
00576 
00577     protected static function unwrapUntranslatable( $text ) {
00578         $text = str_replace( '!N!', "\n", $text );
00579         $text = preg_replace( '~<span class="notranslate">(.*?)</span>~', '\1', $text );
00580 
00581         return $text;
00582     }
00583 
00584     protected function getApertiumSuggestion( $serviceName, $config ) {
00585         self::checkTranslationServiceFailure( $serviceName );
00586 
00587         $page = $this->handle->getKey();
00588         $code = $this->handle->getCode();
00589         $ns = $this->handle->getTitle()->getNamespace();
00590 
00591         $memckey = wfMemckey( 'translate-tmsug-pairs-' . $serviceName );
00592         $pairs = wfGetCache( CACHE_ANYTHING )->get( $memckey );
00593 
00594         if ( !$pairs ) {
00595 
00596             $pairs = array();
00597             $json = Http::get( $config['pairs'], 5 );
00598             $response = FormatJson::decode( $json );
00599 
00600             if ( $json === false ) {
00601                 self::reportTranslationServiceFailure( $serviceName );
00602             } elseif ( !is_object( $response ) ) {
00603                 error_log( __METHOD__ . ': Unable to parse reply: ' . strval( $json ) );
00604                 throw new TranslationHelperException( 'Malformed reply from remote server' );
00605             }
00606 
00607             foreach ( $response->responseData as $pair ) {
00608                 $source = $pair->sourceLanguage;
00609                 $target = $pair->targetLanguage;
00610                 if ( !isset( $pairs[$target] ) ) {
00611                     $pairs[$target] = array();
00612                 }
00613                 $pairs[$target][$source] = true;
00614             }
00615 
00616             wfGetCache( CACHE_ANYTHING )->set( $memckey, $pairs, 60 * 60 * 24 );
00617         }
00618 
00619         if ( isset( $config['codemap'][$code] ) ) {
00620             $code = $config['codemap'][$code];
00621         }
00622 
00623         $code = str_replace( '-', '_', wfBCP47( $code ) );
00624 
00625         if ( !isset( $pairs[$code] ) ) {
00626             throw new TranslationHelperException( 'Unsupported language' );
00627         }
00628 
00629         $suggestions = array();
00630 
00631         $codemap = array_flip( $config['codemap'] );
00632         foreach ( $pairs[$code] as $candidate => $unused ) {
00633             $mwcode = str_replace( '_', '-', strtolower( $candidate ) );
00634 
00635             if ( isset( $codemap[$mwcode] ) ) {
00636                 $mwcode = $codemap[$mwcode];
00637             }
00638 
00639             $text = TranslateUtils::getMessageContent( $page, $mwcode, $ns );
00640             if ( $text === null || TranslateEditAddons::hasFuzzyString( $text ) ) {
00641                 continue;
00642             }
00643 
00644             $title = Title::makeTitleSafe( $ns, "$page/$mwcode" );
00645             if ( $title && TranslateEditAddons::isFuzzy( $title ) ) {
00646                 continue;
00647             }
00648 
00649             $options = self::makeGoogleQueryParams( $text, "$candidate|$code", $config );
00650             $options['postData']['format'] = 'html';
00651 
00652             wfProfileIn( 'TranslateWebServiceRequest-' . $serviceName );
00653             $json = Http::post( $config['url'], $options );
00654             wfProfileOut( 'TranslateWebServiceRequest-' . $serviceName );
00655 
00656             $response = FormatJson::decode( $json );
00657             if ( $json === false || !is_object( $response ) ) {
00658                 self::reportTranslationServiceFailure( $serviceName );
00659             } elseif ( $response->responseStatus !== 200 ) {
00660                 error_log( __METHOD__ .
00661                         " (HTTP {$response->responseStatus}) with ($serviceName ($candidate|$code)): " .
00662                         $response->responseDetails
00663                 );
00664             } else {
00665                 $sug = Sanitizer::decodeCharReferences( $response->responseData->translatedText );
00666                 $sug = trim( $sug );
00667                 $sug = $this->suggestionField( $sug );
00668                 $suggestions[] = Html::rawElement( 'div',
00669                     array( 'title' => $text ),
00670                     self::legend( "$serviceName ($candidate)" ) . $sug . self::clear()
00671                 );
00672             }
00673         }
00674 
00675         if ( !count( $suggestions ) ) {
00676             throw new TranslationHelperException( 'No suggestions' );
00677         }
00678 
00679         $divider = Html::element( 'div', array( 'style' => 'margin-bottom: 0.5ex' ) );
00680 
00681         return implode( "$divider\n", $suggestions );
00682     }
00683 
00684     protected function getYandexSuggestion( $serviceName, $config ) {
00685         self::checkTranslationServiceFailure( $serviceName );
00686 
00687         $page = $this->handle->getKey();
00688         $code = $this->handle->getCode();
00689         $ns = $this->handle->getTitle()->getNamespace();
00690 
00691         $memckey = wfMemckey( 'translate-tmsug-pairs-' . $serviceName );
00692         $pairs = wfGetCache( CACHE_ANYTHING )->get( $memckey );
00693 
00694         if ( !$pairs ) {
00695             $pairs = array();
00696             $json = Http::get( $config['pairs'], $config['timeout'] );
00697             $response = FormatJson::decode( $json );
00698 
00699             if ( $json === false ) {
00700                 self::reportTranslationServiceFailure( $serviceName );
00701             } elseif ( !is_object( $response ) ) {
00702                 error_log( __METHOD__ . ': Unable to parse reply: ' . strval( $json ) );
00703                 throw new TranslationHelperException( 'Malformed reply from remote server' );
00704             }
00705 
00706             foreach ( $response->dirs as $pair ) {
00707                 list( $source, $target ) = explode( '-', $pair );
00708                 if ( !isset( $pairs[$target] ) ) {
00709                     $pairs[$target] = array();
00710                 }
00711                 $pairs[$target][$source] = true;
00712             }
00713 
00714             $weights = array_flip( $config['langorder'] );
00715             $cmpLangs = function ( $lang1, $lang2 ) use ( $weights ) {
00716                 $weight1 = isset( $weights[$lang1] ) ? $weights[$lang1] : PHP_INT_MAX;
00717                 $weight2 = isset( $weights[$lang2] ) ? $weights[$lang2] : PHP_INT_MAX;
00718 
00719                 if ( $weight1 === $weight2 ) {
00720                     return 0;
00721                 }
00722 
00723                 return ( $weight1 < $weight2 ) ? -1 : 1;
00724             };
00725 
00726             foreach ( $pairs as &$langs ) {
00727                 uksort( $langs, $cmpLangs );
00728             }
00729 
00730             wfGetCache( CACHE_ANYTHING )->set( $memckey, $pairs, 60 * 60 * 24 );
00731         }
00732 
00733         if ( !isset( $pairs[$code] ) ) {
00734             throw new TranslationHelperException( 'Unsupported language' );
00735         }
00736 
00737         $suggestions = array();
00738 
00739         foreach ( $pairs[$code] as $candidate => $unused ) {
00740             $text = TranslateUtils::getMessageContent( $page, $candidate, $ns );
00741             if ( $text === null || TranslateEditAddons::hasFuzzyString( $text ) ) {
00742                 continue;
00743             }
00744 
00745             $title = Title::makeTitleSafe( $ns, "$page/$candidate" );
00746             if ( $title && TranslateEditAddons::isFuzzy( $title ) ) {
00747                 continue;
00748             }
00749 
00750             $options = array(
00751                 'timeout' => $config['timeout'],
00752                 'postData' => array(
00753                     'lang' => "$candidate-$code",
00754                     'text' => $text,
00755                 )
00756             );
00757             wfProfileIn( 'TranslateWebServiceRequest-' . $serviceName );
00758             $json = Http::post( $config['url'], $options );
00759             wfProfileOut( 'TranslateWebServiceRequest-' . $serviceName );
00760             $response = FormatJson::decode( $json );
00761 
00762             if ( $json === false || !is_object( $response ) ) {
00763                 self::reportTranslationServiceFailure( $serviceName );
00764             } elseif ( $response->code !== 200 ) {
00765                 error_log( __METHOD__ . " (HTTP {$response->code}) with ($serviceName ($candidate|$code))" );
00766             } else {
00767                 $sug = Sanitizer::decodeCharReferences( $response->text[0] );
00768                 $sug = $this->suggestionField( $sug );
00769                 $suggestions[] = Html::rawElement( 'div',
00770                     array( 'title' => $text ),
00771                     self::legend( "$serviceName ($candidate)" ) . $sug . self::clear()
00772                 );
00773                 if ( count( $suggestions ) === $config['langlimit'] ) {
00774                     break;
00775                 }
00776             }
00777         }
00778 
00779         if ( $suggestions === array() ) {
00780             throw new TranslationHelperException( 'No suggestions' );
00781         }
00782 
00783         $divider = Html::element( 'div', array( 'style' => 'margin-bottom: 0.5ex' ) );
00784 
00785         return implode( "$divider\n", $suggestions );
00786     }
00787 
00788     public function getDefinitionBox() {
00789         $this->mustHaveDefinition();
00790         $en = $this->getDefinition();
00791 
00792         $title = Linker::link(
00793             SpecialPage::getTitleFor( 'Translate' ),
00794             htmlspecialchars( $this->group->getLabel() ),
00795             array(),
00796             array(
00797                 'group' => $this->group->getId(),
00798                 'language' => $this->handle->getCode()
00799             )
00800         );
00801 
00802         $label =
00803             wfMessage( 'translate-edit-definition' )->text() .
00804                 wfMessage( 'word-separator' )->text() .
00805                 wfMessage( 'parentheses', $title )->text();
00806 
00807         // Source language object
00808         $sl = Language::factory( $this->group->getSourceLanguage() );
00809 
00810         $dialogID = $this->dialogID();
00811         $id = Sanitizer::escapeId( "def-$dialogID" );
00812         $msg = $this->adder( $id, $sl ) . "\n" . Html::rawElement( 'div',
00813             array(
00814                 'class' => 'mw-translate-edit-deftext',
00815                 'dir' => $sl->getDir(),
00816                 'lang' => $sl->getCode(),
00817             ),
00818             TranslateUtils::convertWhiteSpaceToHTML( $en )
00819         );
00820 
00821         $msg .= $this->wrapInsert( $id, $en );
00822 
00823         $class = array( 'class' => 'mw-sp-translate-edit-definition mw-translate-edit-definition' );
00824 
00825         return TranslateUtils::fieldset( $label, $msg, $class );
00826     }
00827 
00828     public function getTranslationDisplayBox() {
00829         $en = $this->getTranslation();
00830         if ( $en === null ) {
00831             return null;
00832         }
00833         $label = wfMessage( 'translate-edit-translation' )->text();
00834         $class = array( 'class' => 'mw-translate-edit-translation' );
00835         $msg = Html::rawElement( 'span',
00836             array( 'class' => 'mw-translate-edit-translationtext' ),
00837             TranslateUtils::convertWhiteSpaceToHTML( $en )
00838         );
00839 
00840         return TranslateUtils::fieldset( $label, $msg, $class );
00841     }
00842 
00843     public function getCheckBox() {
00844         $this->mustBeKnownMessage();
00845 
00846         global $wgTranslateDocumentationLanguageCode;
00847 
00848         $context = RequestContext::getMain();
00849         $title = $context->getOutput()->getTitle();
00850         list( $alias, ) = SpecialPageFactory::resolveAlias( $title->getText() );
00851 
00852         $tux = SpecialTranslate::isBeta( $context->getRequest() )
00853             && $title->isSpecialPage()
00854             && ( $alias === 'Translate' );
00855 
00856         $formattedChecks = $tux ?
00857             FormatJson::encode( array() ) :
00858             Html::element( 'div', array( 'class' => 'mw-translate-messagechecks' ) );
00859 
00860         $page = $this->handle->getKey();
00861         $translation = $this->getTranslation();
00862         $code = $this->handle->getCode();
00863         $en = $this->getDefinition();
00864 
00865         if ( strval( $translation ) === '' ) {
00866             return $formattedChecks;
00867         }
00868 
00869         if ( $code === $wgTranslateDocumentationLanguageCode ) {
00870             return null;
00871         }
00872 
00873         // We need to get the primary group of the message. It may differ from
00874         // the supplied group (aggregate groups, dynamic groups).
00875         $checker = $this->handle->getGroup()->getChecker();
00876         if ( !$checker ) {
00877             return null;
00878         }
00879 
00880         $message = new FatMessage( $page, $en );
00881         // Take the contents from edit field as a translation
00882         $message->setTranslation( $translation );
00883 
00884         $checks = $checker->checkMessage( $message, $code );
00885         if ( !count( $checks ) ) {
00886             return $formattedChecks;
00887         }
00888 
00889         $checkMessages = array();
00890 
00891         foreach ( $checks as $checkParams ) {
00892             $key = array_shift( $checkParams );
00893             $checkMessages[] = $context->msg( $key, $checkParams )->parse();
00894         }
00895 
00896         if ( $tux ) {
00897             $formattedChecks = FormatJson::encode( $checkMessages );
00898         } else {
00899             $formattedChecks = Html::rawElement(
00900                 'div',
00901                 array( 'class' => 'mw-translate-messagechecks' ),
00902                 TranslateUtils::fieldset(
00903                     $context->msg( 'translate-edit-warnings' )->escaped(),
00904                     implode( '<hr />', $checkMessages ),
00905                     array( 'class' => 'mw-sp-translate-edit-warnings' )
00906                 )
00907             );
00908         }
00909 
00910         return $formattedChecks;
00911     }
00912 
00913     public function getOtherLanguagesBox() {
00914         $code = $this->handle->getCode();
00915         $page = $this->handle->getKey();
00916         $ns = $this->handle->getTitle()->getNamespace();
00917 
00918         $boxes = array();
00919         foreach ( self::getFallbacks( $code ) as $fbcode ) {
00920             $text = TranslateUtils::getMessageContent( $page, $fbcode, $ns );
00921             if ( $text === null ) {
00922                 continue;
00923             }
00924 
00925             $context = RequestContext::getMain();
00926             $label = TranslateUtils::getLanguageName( $fbcode, $context->getLanguage()->getCode() ) .
00927                 $context->msg( 'word-separator' )->text() .
00928                 $context->msg( 'parentheses', wfBCP47( $fbcode ) )->text();
00929 
00930             $target = Title::makeTitleSafe( $ns, "$page/$fbcode" );
00931             if ( $target ) {
00932                 $label = self::ajaxEditLink( $target, htmlspecialchars( $label ) );
00933             }
00934 
00935             $dialogID = $this->dialogID();
00936             $id = Sanitizer::escapeId( "other-$fbcode-$dialogID" );
00937 
00938             $params = array( 'class' => 'mw-translate-edit-item' );
00939 
00940             $display = TranslateUtils::convertWhiteSpaceToHTML( $text );
00941             $display = Html::rawElement( 'div', array(
00942                     'lang' => $fbcode,
00943                     'dir' => Language::factory( $fbcode )->getDir() ),
00944                 $display
00945             );
00946 
00947             $contents = self::legend( $label ) . "\n" . $this->adder( $id, $fbcode ) .
00948                 $display . self::clear();
00949 
00950             $boxes[] = Html::rawElement( 'div', $params, $contents ) .
00951                 $this->wrapInsert( $id, $text );
00952         }
00953 
00954         if ( count( $boxes ) ) {
00955             $sep = Html::element( 'hr', array( 'class' => 'mw-translate-sep' ) );
00956 
00957             return TranslateUtils::fieldset(
00958                 wfMessage(
00959                     'translate-edit-in-other-languages',
00960                     $page
00961                 )->escaped(),
00962                 implode( "$sep\n", $boxes ),
00963                 array( 'class' => 'mw-sp-translate-edit-inother' )
00964             );
00965         }
00966 
00967         return null;
00968     }
00969 
00970     public function getSeparatorBox() {
00971         return Html::element( 'div', array( 'class' => 'mw-translate-edit-extra' ) );
00972     }
00973 
00974     public function getDocumentationBox() {
00975         global $wgTranslateDocumentationLanguageCode;
00976 
00977         if ( !$wgTranslateDocumentationLanguageCode ) {
00978             throw new TranslationHelperException( 'Message documentation language code is not defined' );
00979         }
00980 
00981         $context = RequestContext::getMain();
00982         $page = $this->handle->getKey();
00983         $ns = $this->handle->getTitle()->getNamespace();
00984 
00985         $title = Title::makeTitle( $ns, $page . '/' . $wgTranslateDocumentationLanguageCode );
00986         $edit = self::ajaxEditLink(
00987             $title,
00988             $context->msg( 'translate-edit-contribute' )->escaped()
00989         );
00990         $info = TranslateUtils::getMessageContent( $page, $wgTranslateDocumentationLanguageCode, $ns );
00991 
00992         $class = 'mw-sp-translate-edit-info';
00993 
00994         $gettext = $this->formatGettextComments();
00995         if ( $info !== null && $gettext ) {
00996             $info .= Html::element( 'hr' );
00997         }
00998         $info .= $gettext;
00999 
01000         // The information is most likely in English
01001         $divAttribs = array( 'dir' => 'ltr', 'lang' => 'en', 'class' => 'mw-content-ltr' );
01002 
01003         if ( strval( $info ) === '' ) {
01004             $info = $context->msg( 'translate-edit-no-information' )->text();
01005             $class = 'mw-sp-translate-edit-noinfo';
01006             $lang = $context->getLanguage();
01007             // The message saying that there's no info, should be translated
01008             $divAttribs = array( 'dir' => $lang->getDir(), 'lang' => $lang->getCode() );
01009         }
01010         $class .= ' mw-sp-translate-message-documentation';
01011 
01012         $contents = $context->getOutput()->parse( $info );
01013         // Remove whatever block element wrapup the parser likes to add
01014         $contents = preg_replace( '~^<([a-z]+)>(.*)</\1>$~us', '\2', $contents );
01015 
01016         return TranslateUtils::fieldset(
01017             $context->msg( 'translate-edit-information' )->rawParams( $edit )->escaped(),
01018             Html::rawElement( 'div', $divAttribs, $contents ), array( 'class' => $class )
01019         );
01020     }
01021 
01022     protected function formatGettextComments() {
01023         if ( !$this->handle->isValid() ) {
01024             return '';
01025         }
01026 
01027         // We need to get the primary group to get the correct file
01028         // So $group can be different from $this->group
01029         $group = $this->handle->getGroup();
01030         if ( !$group instanceof FileBasedMessageGroup ) {
01031             return '';
01032         }
01033 
01034         $ffs = $group->getFFS();
01035         if ( $ffs instanceof GettextFFS ) {
01036             global $wgContLang;
01037             $mykey = $wgContLang->lcfirst( $this->handle->getKey() );
01038             $mykey = str_replace( ' ', '_', $mykey );
01039             $data = $ffs->read( $group->getSourceLanguage() );
01040             $help = $data['TEMPLATE'][$mykey]['comments'];
01041             // Do not display an empty comment. That's no help and takes up unnecessary space.
01042             $conf = $group->getConfiguration();
01043             if ( isset( $conf['BASIC']['codeBrowser'] ) ) {
01044                 $out = '';
01045                 $pattern = $conf['BASIC']['codeBrowser'];
01046                 $pattern = str_replace( '%FILE%', '\1', $pattern );
01047                 $pattern = str_replace( '%LINE%', '\2', $pattern );
01048                 $pattern = "[$pattern \\1:\\2]";
01049                 foreach ( $help as $type => $lines ) {
01050                     if ( $type === ':' ) {
01051                         $files = '';
01052                         foreach ( $lines as $line ) {
01053                             $files .= ' ' . preg_replace( '/([^ :]+):(\d+)/', $pattern, $line );
01054                         }
01055                         $out .= "<nowiki>#:</nowiki> $files<br />";
01056                     } else {
01057                         foreach ( $lines as $line ) {
01058                             $out .= "<nowiki>#$type</nowiki> $line<br />";
01059                         }
01060                     }
01061                 }
01062 
01063                 return "$out";
01064             }
01065         }
01066 
01067         return '';
01068     }
01069 
01070     protected function getPageDiff() {
01071         $this->mustBeKnownMessage();
01072 
01073         $title = $this->handle->getTitle();
01074         $key = $this->handle->getKey();
01075 
01076         if ( !$title->exists() ) {
01077             return null;
01078         }
01079 
01080         $definitionTitle = Title::makeTitleSafe( $title->getNamespace(), "$key/en" );
01081         if ( !$definitionTitle || !$definitionTitle->exists() ) {
01082             return null;
01083         }
01084 
01085         $db = wfGetDB( DB_MASTER );
01086         $conds = array(
01087             'rt_page' => $title->getArticleID(),
01088             'rt_type' => RevTag::getType( 'tp:transver' ),
01089         );
01090         $options = array(
01091             'ORDER BY' => 'rt_revision DESC',
01092         );
01093 
01094         $latestRevision = $definitionTitle->getLatestRevID();
01095 
01096         $translationRevision = $db->selectField( 'revtag', 'rt_value', $conds, __METHOD__, $options );
01097         if ( $translationRevision === false ) {
01098             return null;
01099         }
01100 
01101         // Using newFromId instead of newFromTitle, because the page might have been renamed
01102         $oldrev = Revision::newFromId( $translationRevision );
01103         if ( !$oldrev ) {
01104             // And someone might still have deleted it
01105             return null;
01106         }
01107         $oldtext = $oldrev->getText();
01108         $newtext = Revision::newFromTitle( $definitionTitle, $latestRevision )->getText();
01109 
01110         if ( $oldtext === $newtext ) {
01111             return null;
01112         }
01113 
01114         $diff = new DifferenceEngine;
01115         if ( method_exists( 'DifferenceEngine', 'setTextLanguage' ) ) {
01116             $diff->setTextLanguage( $this->group->getSourceLanguage() );
01117         }
01118         $diff->setText( $oldtext, $newtext );
01119         $diff->setReducedLineNumbers();
01120         $diff->showDiffStyle();
01121 
01122         return $diff->getDiff(
01123             wfMessage( 'tpt-diff-old' )->escaped(),
01124             wfMessage( 'tpt-diff-new' )->escaped()
01125         );
01126     }
01127 
01128     protected function getLastDiff() {
01129         // Shortcuts
01130         $title = $this->handle->getTitle();
01131         $latestRevId = $title->getLatestRevID();
01132         $previousRevId = $title->getPreviousRevisionID( $latestRevId );
01133 
01134         $latestRev = Revision::newFromTitle( $title, $latestRevId );
01135         $previousRev = Revision::newFromTitle( $title, $previousRevId );
01136 
01137         $diffText = '';
01138 
01139         if ( $latestRev && $previousRev ) {
01140             $latest = $latestRev->getText();
01141             $previous = $previousRev->getText();
01142             if ( $previous !== $latest ) {
01143                 $diff = new DifferenceEngine;
01144                 if ( method_exists( 'DifferenceEngine', 'setTextLanguage' ) ) {
01145                     $diff->setTextLanguage( $this->getTargetLanguage() );
01146                 }
01147                 $diff->setText( $previous, $latest );
01148                 $diff->setReducedLineNumbers();
01149                 $diff->showDiffStyle();
01150                 $diffText = $diff->getDiff( false, false );
01151             }
01152         }
01153 
01154         if ( !$latestRev ) {
01155             return null;
01156         }
01157 
01158         $context = RequestContext::getMain();
01159         $user = $latestRev->getUserText( Revision::FOR_THIS_USER, $context->getUser() );
01160         $comment = $latestRev->getComment();
01161 
01162         if ( $diffText === '' ) {
01163             if ( strval( $comment ) !== '' ) {
01164                 $text = $context->msg( 'translate-dynagroup-byc', $user, $comment )->escaped();
01165             } else {
01166                 $text = $context->msg( 'translate-dynagroup-by', $user )->escaped();
01167             }
01168         } else {
01169             if ( strval( $comment ) !== '' ) {
01170                 $text = $context->msg( 'translate-dynagroup-lastc', $user, $comment )->escaped();
01171             } else {
01172                 $text = $context->msg( 'translate-dynagroup-last', $user )->escaped();
01173             }
01174         }
01175 
01176         return TranslateUtils::fieldset(
01177             $text,
01178             $diffText,
01179             array( 'class' => 'mw-sp-translate-latestchange' )
01180         );
01181     }
01182 
01187     protected static function legend( $label ) {
01188         # Float it to the opposite direction
01189         return Html::rawElement( 'div', array( 'class' => 'mw-translate-legend' ), $label );
01190     }
01191 
01195     protected static function clear() {
01196         return Html::element( 'div', array( 'style' => 'clear:both;' ) );
01197     }
01198 
01203     protected static function getFallbacks( $code ) {
01204         global $wgTranslateLanguageFallbacks;
01205 
01206         // User preference has the final say
01207         $user = RequestContext::getMain()->getUser();
01208         $preference = $user->getOption( 'translate-editlangs' );
01209         if ( $preference !== 'default' ) {
01210             $fallbacks = array_map( 'trim', explode( ',', $preference ) );
01211             foreach ( $fallbacks as $k => $v ) {
01212                 if ( $v === $code ) {
01213                     unset( $fallbacks[$k] );
01214                 }
01215             }
01216 
01217             return $fallbacks;
01218         }
01219 
01220         // Global configuration settings
01221         $fallbacks = array();
01222         if ( isset( $wgTranslateLanguageFallbacks[$code] ) ) {
01223             $fallbacks = (array)$wgTranslateLanguageFallbacks[$code];
01224         }
01225 
01226         $list = Language::getFallbacksFor( $code );
01227         array_pop( $list ); // Get 'en' away from the end
01228         $fallbacks = array_merge( $list, $fallbacks );
01229 
01230         return array_unique( $fallbacks );
01231     }
01232 
01236     public function getLazySuggestionBox() {
01237         $this->mustBeKnownMessage();
01238         if ( !$this->handle->getCode() ) {
01239             return null;
01240         }
01241 
01242         $url = SpecialPage::getTitleFor( 'Translate', 'editpage' )->getLocalUrl( array(
01243             'suggestions' => 'only',
01244             'page' => $this->handle->getTitle()->getPrefixedDbKey(),
01245             'loadgroup' => $this->group->getId(),
01246         ) );
01247         $url = Xml::encodeJsVar( $url );
01248 
01249         $id = Sanitizer::escapeId( 'tm-lazysug-' . $this->dialogID() );
01250         $target = self::jQueryPathId( $id );
01251 
01252         $script = Html::inlineScript( "jQuery($target).load($url)" );
01253         $spinner = Html::element( 'div', array( 'class' => 'mw-ajax-loader' ) );
01254 
01255         return Html::rawElement( 'div', array( 'id' => $id ), $script . $spinner );
01256     }
01257 
01261     public function dialogID() {
01262         $hash = sha1( $this->handle->getTitle()->getPrefixedDbKey() );
01263 
01264         return substr( $hash, 0, 4 );
01265     }
01266 
01272     public function adder( $source, $lang ) {
01273         if ( !$this->editMode ) {
01274             return '';
01275         }
01276         $target = self::jQueryPathId( $this->getTextareaId() );
01277         $source = self::jQueryPathId( $source );
01278         $dir = wfGetLangObj( $lang )->getDir();
01279         $params = array(
01280             'onclick' => "jQuery($target).val(jQuery($source).text()).focus(); return false;",
01281             'href' => '#',
01282             'title' => wfMessage( 'translate-use-suggestion' )->text(),
01283             'class' => 'mw-translate-adder mw-translate-adder-' . $dir,
01284         );
01285 
01286         return Html::element( 'a', $params, '↓' );
01287     }
01288 
01294     public function wrapInsert( $id, $text ) {
01295         return Html::element( 'pre', array( 'id' => $id, 'style' => 'display: none;' ), $text );
01296     }
01297 
01302     public function suggestionField( $text ) {
01303         static $counter = 0;
01304 
01305         $code = $this->getTargetLanguage();
01306 
01307         $counter++;
01308         $dialogID = $this->dialogID();
01309         $id = Sanitizer::escapeId( "tmsug-$dialogID-$counter" );
01310         $contents = Html::rawElement( 'div', array( 'lang' => $code,
01311                 'dir' => Language::factory( $code )->getDir() ),
01312             TranslateUtils::convertWhiteSpaceToHTML( $text ) );
01313         $contents .= $this->wrapInsert( $id, $text );
01314 
01315         return $this->adder( $id, $code ) . "\n" . $contents;
01316     }
01317 
01324     public static function ajaxEditLink( $target, $text ) {
01325         $handle = new MessageHandle( $target );
01326         $groupId = MessageIndex::getPrimaryGroupId( $handle );
01327 
01328         $params = array();
01329         $params['action'] = 'edit';
01330         $params['loadgroup'] = $groupId;
01331 
01332         $jsEdit = TranslationEditPage::jsEdit( $target, $groupId, 'dialog' );
01333 
01334         return Linker::link( $target, $text, $jsEdit, $params );
01335     }
01336 
01342     public static function jQueryPathId( $id ) {
01343         $id = preg_replace( '/[^A-Za-z0-9_-]/', '\\\\$0', $id );
01344 
01345         return Xml::encodeJsVar( "#$id" );
01346     }
01347 
01351     protected static $serviceFailureCount = 5;
01356     protected static $serviceFailurePeriod = 900;
01357 
01363     public static function checkTranslationServiceFailure( $service ) {
01364         $key = wfMemckey( "translate-service-$service" );
01365         $value = wfGetCache( CACHE_ANYTHING )->get( $key );
01366         if ( !is_string( $value ) ) {
01367             return;
01368         }
01369         list( $count, $failed ) = explode( '|', $value, 2 );
01370 
01371         if ( $failed + ( 2 * self::$serviceFailurePeriod ) < wfTimestamp() ) {
01372             if ( $count >= self::$serviceFailureCount ) {
01373                 error_log( "Translation service $service (was) restored" );
01374             }
01375             wfGetCache( CACHE_ANYTHING )->delete( $key );
01376 
01377             return;
01378         } elseif ( $failed + self::$serviceFailurePeriod < wfTimestamp() ) {
01379             /* We are in suspicious mode and one failure is enough to update
01380              * failed timestamp. If the service works however, let's use it.
01381              * Previous failures are forgotten after another failure period
01382              * has passed */
01383             return;
01384         }
01385 
01386         if ( $count >= self::$serviceFailureCount ) {
01387             throw new TranslationHelperException( "web service $service is temporarily disabled" );
01388         }
01389     }
01390 
01396     public static function reportTranslationServiceFailure( $service ) {
01397         $key = wfMemckey( "translate-service-$service" );
01398         $value = wfGetCache( CACHE_ANYTHING )->get( $key );
01399         if ( !is_string( $value ) ) {
01400             $count = 0;
01401         } else {
01402             list( $count, ) = explode( '|', $value, 2 );
01403         }
01404 
01405         $count += 1;
01406         $failed = wfTimestamp();
01407         wfGetCache( CACHE_ANYTHING )->set( $key, "$count|$failed", self::$serviceFailurePeriod * 5 );
01408 
01409         if ( $count == self::$serviceFailureCount ) {
01410             error_log( "Translation service $service suspended" );
01411         } elseif ( $count > self::$serviceFailureCount ) {
01412             error_log( "Translation service $service still suspended" );
01413         }
01414 
01415         throw new TranslationHelperException( "web service $service failed to provide valid response" );
01416     }
01417 
01418     public static function addModules( OutputPage $out ) {
01419         $modules = array( 'ext.translate.quickedit' );
01420         wfRunHooks( 'TranslateBeforeAddModules', array( &$modules ) );
01421         $out->addModules( $modules );
01422 
01423         // Might be needed, but ajax doesn't load it
01424         // Globals :(
01425         $diff = new DifferenceEngine;
01426         $diff->showDiffStyle();
01427     }
01428 
01430     protected function mustBeKnownMessage() {
01431         if ( !$this->group ) {
01432             throw new TranslationHelperException( 'unknown group' );
01433         }
01434     }
01435 
01437     protected function mustBeTranslation() {
01438         if ( !$this->handle->getCode() ) {
01439             throw new TranslationHelperException( 'editing source language' );
01440         }
01441     }
01442 
01444     protected function mustHaveDefinition() {
01445         if ( strval( $this->getDefinition() ) === '' ) {
01446             throw new TranslationHelperException( 'message does not have definition' );
01447         }
01448     }
01449 }
01450 
01460 class TranslationHelperException extends MWException {
01461 }
Generated on Tue Oct 29 00:00:26 2013 for MediaWiki Translate Extension by  doxygen 1.6.3