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
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 '*',
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
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
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
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
00462 $boxes = array_filter( $boxes );
00463
00464
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
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
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
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
00874
00875 $checker = $this->handle->getGroup()->getChecker();
00876 if ( !$checker ) {
00877 return null;
00878 }
00879
00880 $message = new FatMessage( $page, $en );
00881
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
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
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
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
01028
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
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
01102 $oldrev = Revision::newFromId( $translationRevision );
01103 if ( !$oldrev ) {
01104
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
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
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
01221 $fallbacks = array();
01222 if ( isset( $wgTranslateLanguageFallbacks[$code] ) ) {
01223 $fallbacks = (array)$wgTranslateLanguageFallbacks[$code];
01224 }
01225
01226 $list = Language::getFallbacksFor( $code );
01227 array_pop( $list );
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
01380
01381
01382
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
01424
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 }