'MicrosoftWebService', 'apertium' => 'ApertiumWebService', 'yandex' => 'YandexWebService', 'google' => 'GoogleTranslateWebService', 'remote-ttmserver' => 'RemoteTTMServerWebService', 'cxserver' => 'CxserverWebService', 'restbase' => 'RESTBaseWebService', 'caighdean' => 'CaighdeanWebService', ]; if ( !isset( $config['timeout'] ) ) { $config['timeout'] = 3; } $class = $handlers[$config['type']] ?? null; if ( $class ) { $obj = new $class( $name, $config ); $obj->setLogger( LoggerFactory::getInstance( 'translationservices' ) ); return $obj; } return null; } /** * Gets the name of this service, for example to display it for the user. * * @return string Plain text name for this service. * @since 2014.02 */ public function getName() { return $this->service; } /** * Get queries for this service. Queries from multiple services can be * collected and run asynchronously with QueryAggregator. * * @param string $text Source text * @param string $from Source language * @param string $to Target language * @return TranslationQuery[] * @since 2015.12 * @throws TranslationWebServiceConfigurationException */ public function getQueries( $text, $from, $to ) { $from = $this->mapCode( $from ); $to = $this->mapCode( $to ); try { return [ $this->getQuery( $text, $from, $to ) ]; } catch ( TranslationWebServiceException $e ) { $this->reportTranslationServiceFailure( $e->getMessage() ); return []; } catch ( TranslationWebServiceInvalidInputException $e ) { // Not much we can do about this, just ignore. return []; } } /** * Get the web service specific response returned by QueryAggregator. * * @param TranslationQueryResponse $response * @return mixed|null Returns null on error. * @since 2015.12 */ public function getResultData( TranslationQueryResponse $response ) { if ( $response->getStatusCode() !== 200 ) { $this->reportTranslationServiceFailure( 'STATUS: ' . $response->getStatusMessage() . "\n" . 'BODY: ' . $response->getBody() ); return null; } try { return $this->parseResponse( $response ); } catch ( TranslationWebServiceException $e ) { $this->reportTranslationServiceFailure( $e->getMessage() ); return null; } } /** * Returns the type of this web service. * @see \MediaWiki\Extension\Translate\TranslatorInterface\Aid\TranslationAid::getTypes * @return string */ abstract public function getType(); /* Service api */ /** * Map a MediaWiki (almost standard) language code to the code used by the * translation service. * * @param string $code MediaWiki language code. * @return string Translation service language code. */ abstract protected function mapCode( $code ); /** * Get the list of supported language pairs for the web service. The codes * should be the ones used by the service. Caching is handled by the public * getSupportedLanguagePairs. * * @return array $list[source language][target language] = true * @throws TranslationWebServiceException * @throws TranslationWebServiceConfigurationException */ abstract protected function doPairs(); /** * Get the query. See getQueries for the public method. * * @param string $text Text to translate. * @param string $from Language code of the text, as used by the service. * @param string $to Language code of the translation, as used by the service. * @return TranslationQuery * @since 2015.02 * @throws TranslationWebServiceException * @throws TranslationWebServiceConfigurationException * @throws TranslationWebServiceInvalidInputException */ abstract protected function getQuery( $text, $from, $to ); /** * Get the response. See getResultData for the public method. * * @param TranslationQueryResponse $response * @return string * @since 2015.02 * @throws TranslationWebServiceException */ abstract protected function parseResponse( TranslationQueryResponse $response ); /* Default implementation */ /** @var string Name of this webservice. */ protected $service; /** @var array */ protected $config; /** @var LoggerInterface */ protected $logger; /** * @param string $service Name of the webservice * @param array $config */ protected function __construct( $service, $config ) { $this->service = $service; $this->config = $config; } /** * Test whether given language pair is supported by the service. * * @param string $from Source language * @param string $to Target language * @return bool * @since 2015.12 * @throws TranslationWebServiceConfigurationException */ public function isSupportedLanguagePair( $from, $to ) { $pairs = $this->getSupportedLanguagePairs(); $from = $this->mapCode( $from ); $to = $this->mapCode( $to ); return isset( $pairs[$from][$to] ); } /** * @see self::doPairs * @return array * @throws TranslationWebServiceConfigurationException */ protected function getSupportedLanguagePairs() { $cache = ObjectCache::getInstance( CACHE_ANYTHING ); return $cache->getWithSetCallback( $cache->makeKey( 'translate-tmsug-pairs-' . $this->service ), $cache::TTL_DAY, function ( &$ttl ) use ( $cache ) { try { $pairs = $this->doPairs(); } catch ( Exception $e ) { $pairs = []; $this->reportTranslationServiceFailure( $e->getMessage() ); $ttl = $cache::TTL_UNCACHEABLE; } return $pairs; } ); } /** * Some mangling that tries to keep some parts of the message unmangled * by the translation service. Most of them support either class=notranslate * or translate=no. * @param string $text * @return string */ protected function wrapUntranslatable( $text ) { $text = str_replace( "\n", '!N!', $text ); $pattern = '~%[^% ]+%|\$\d|{VAR:[^}]+}|{?{(PLURAL|GRAMMAR|GENDER):[^|]+\||%(\d\$)?[sd]~'; $wrap = '\0'; return preg_replace( $pattern, $wrap, $text ); } /** * Undo the hopyfully untouched mangling done by wrapUntranslatable. * @param string $text * @return string */ protected function unwrapUntranslatable( $text ) { $text = str_replace( '!N!', "\n", $text ); $pattern = '~(.*?)~'; return preg_replace( $pattern, '\1', $text ); } /* Failure handling and suspending */ public function setLogger( LoggerInterface $logger ) { $this->logger = $logger; } /** * @var int How many failures during failure period need to happen to * consider the service being temporarily off-line. */ protected $serviceFailureCount = 5; /** * @var int How long after the last detected failure we clear the status and * try again. */ protected $serviceFailurePeriod = 900; /** * Checks whether the service has exceeded failure count * @return bool */ public function checkTranslationServiceFailure() { $service = $this->service; $cache = ObjectCache::getInstance( CACHE_ANYTHING ); $key = $cache->makeKey( "translate-service-$service" ); $value = $cache->get( $key ); if ( !is_string( $value ) ) { return false; } list( $count, $failed ) = explode( '|', $value, 2 ); if ( $failed + ( 2 * $this->serviceFailurePeriod ) < wfTimestamp() ) { if ( $count >= $this->serviceFailureCount ) { $this->logger->warning( "Translation service $service (was) restored" ); } $cache->delete( $key ); return false; } elseif ( $failed + $this->serviceFailurePeriod < wfTimestamp() ) { /* We are in suspicious mode and one failure is enough to update * failed timestamp. If the service works however, let's use it. * Previous failures are forgotten after another failure period * has passed */ return false; } // Check the failure count against the limit return $count >= $this->serviceFailureCount; } /** * Increases the failure count for this service * @param string $msg */ protected function reportTranslationServiceFailure( $msg ) { $service = $this->service; $this->logger->warning( "Translation service $service problem: $msg" ); $cache = ObjectCache::getInstance( CACHE_ANYTHING ); $key = $cache->makeKey( "translate-service-$service" ); $value = $cache->get( $key ); if ( !is_string( $value ) ) { $count = 0; } else { list( $count, ) = explode( '|', $value, 2 ); } $count++; $failed = wfTimestamp(); $cache->set( $key, "$count|$failed", $this->serviceFailurePeriod * 5 ); if ( $count === $this->serviceFailureCount ) { $this->logger->error( "Translation service $service suspended" ); } elseif ( $count > $this->serviceFailureCount ) { $this->logger->warning( "Translation service $service still suspended" ); } } }