Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
TranslationWebService.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\WebService;
5
6use Exception;
7use MediaWiki\Logger\LoggerFactory;
8use MediaWiki\MediaWikiServices;
9use ObjectCache;
10use Psr\Log\LoggerAwareInterface;
11use Psr\Log\LoggerInterface;
12
24abstract class TranslationWebService implements LoggerAwareInterface {
25 /* Public api */
26
31 public static function factory( string $serviceName, array $config ): ?TranslationWebService {
32 $handlers = [
33 'microsoft' => [
34 'class' => MicrosoftWebService::class,
35 'deps' => [ 'HttpRequestFactory' ]
36 ],
37 'apertium' => [
38 'class' => ApertiumWebService::class,
39 'deps' => [ 'HttpRequestFactory' ]
40 ],
41 'yandex' => [
42 'class' => YandexWebService::class,
43 'deps' => [ 'HttpRequestFactory' ]
44 ],
45 'google' => [
46 'class' => GoogleTranslateWebService::class,
47 'deps' => [ 'HttpRequestFactory' ]
48 ],
49 'remote-ttmserver' => [
50 'class' => RemoteTTMServerWebService::class
51 ],
52 'cxserver' => [
53 'class' => CxserverWebService::class,
54 'deps' => [ 'HttpRequestFactory' ]
55 ],
56 'restbase' => [
57 'class' => RESTBaseWebService::class,
58 'deps' => [ 'HttpRequestFactory' ]
59 ],
60 'caighdean' => [
61 'class' => CaighdeanWebService::class
62 ],
63 ];
64
65 if ( !isset( $config['timeout'] ) ) {
66 $config['timeout'] = 3;
67 }
68
69 $serviceDetails = $handlers[$config['type']] ?? null;
70 if ( $serviceDetails ) {
71 $objectFactory = MediaWikiServices::getInstance()->getObjectFactory();
72 $spec = [
73 'class' => $serviceDetails['class'],
74 'args' => [ $serviceName, $config ],
75 'services' => $serviceDetails['deps'] ?? [],
76 ];
77
78 // @phan-suppress-next-line PhanTypeInvalidCallableArraySize due to annotations on createObject?
79 $serviceObject = $objectFactory->createObject( $spec );
80 if ( $serviceObject instanceof LoggerAwareInterface ) {
81 $serviceObject->setLogger( LoggerFactory::getInstance( 'translationservices' ) );
82 }
83
84 return $serviceObject;
85 }
86
87 return null;
88 }
89
94 public function getName(): string {
95 return $this->service;
96 }
97
105 public function getQueries( string $text, string $sourceLanguage, string $targetLanguage ): array {
106 $from = $this->mapCode( $sourceLanguage );
107 $to = $this->mapCode( $targetLanguage );
108
109 try {
110 return [ $this->getQuery( $text, $from, $to ) ];
111 } catch ( TranslationWebServiceException $e ) {
112 $this->reportTranslationServiceFailure( $e->getMessage() );
113 return [];
114 } catch ( TranslationWebServiceInvalidInputException $e ) {
115 // Not much we can do about this, just ignore.
116 return [];
117 }
118 }
119
125 public function getResultData( TranslationQueryResponse $response ) {
126 if ( $response->getStatusCode() !== 200 ) {
127 $this->reportTranslationServiceFailure(
128 'STATUS: ' . $response->getStatusMessage() . "\n" .
129 'BODY: ' . $response->getBody()
130 );
131 return null;
132 }
133
134 try {
135 return $this->parseResponse( $response );
136 } catch ( TranslationWebServiceException $e ) {
137 $this->reportTranslationServiceFailure( $e->getMessage() );
138 return null;
139 }
140 }
141
146 abstract public function getType(): string;
147
148 /* Service api */
149
154 abstract protected function mapCode( string $code ): string;
155
164 abstract protected function doPairs(): array;
165
176 abstract protected function getQuery(
177 string $text, string $sourceLanguage, string $targetLanguage
179
185 abstract protected function parseResponse( TranslationQueryResponse $response );
186
187 /* Default implementation */
188
190 protected $service;
192 protected $config;
194 protected $logger;
195
196 public function __construct( string $service, array $config ) {
197 $this->service = $service;
198 $this->config = $config;
199 }
200
206 public function isSupportedLanguagePair( string $sourceLanguage, string $targetLanguage ): bool {
207 $pairs = $this->getSupportedLanguagePairs();
208 $from = $this->mapCode( $sourceLanguage );
209 $to = $this->mapCode( $targetLanguage );
210
211 return isset( $pairs[$sourceLanguage][$targetLanguage] );
212 }
213
218 protected function getSupportedLanguagePairs(): array {
219 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
220
221 return $cache->getWithSetCallback(
222 $cache->makeKey( 'translate-tmsug-pairs-' . $this->service ),
223 $cache::TTL_DAY,
224 function ( &$ttl ) use ( $cache ) {
225 try {
226 $pairs = $this->doPairs();
227 } catch ( Exception $e ) {
228 $pairs = [];
229 $this->reportTranslationServiceFailure( $e->getMessage() );
230 $ttl = $cache::TTL_UNCACHEABLE;
231 }
232
233 return $pairs;
234 }
235 );
236 }
237
243 protected function wrapUntranslatable( string $text ): string {
244 $text = str_replace( "\n", '!N!', $text );
245 $pattern = '~%[^% ]+%|\$\d|{VAR:[^}]+}|{?{(PLURAL|GRAMMAR|GENDER):[^|]+\||%(\d\$)?[sd]~';
246 $wrap = '<span class="notranslate" translate="no">\0</span>';
247 return preg_replace( $pattern, $wrap, $text );
248 }
249
251 protected function unwrapUntranslatable( string $text ): string {
252 $text = str_replace( '!N!', "\n", $text );
253 $pattern = '~<span class="notranslate" translate="no">(.*?)</span>~';
254 return preg_replace( $pattern, '\1', $text );
255 }
256
257 /* Failure handling and suspending */
258
259 public function setLogger( LoggerInterface $logger ): void {
260 $this->logger = $logger;
261 }
262
267 protected $serviceFailureCount = 5;
272 protected $serviceFailurePeriod = 900;
273
275 public function checkTranslationServiceFailure(): bool {
276 $service = $this->service;
277 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
278
279 $key = $cache->makeKey( "translate-service-$service" );
280 $value = $cache->get( $key );
281 if ( !is_string( $value ) ) {
282 return false;
283 }
284
285 list( $count, $failed ) = explode( '|', $value, 2 );
286
287 if ( $failed + ( 2 * $this->serviceFailurePeriod ) < wfTimestamp() ) {
288 if ( $count >= $this->serviceFailureCount ) {
289 $this->logger->warning( "Translation service $service (was) restored" );
290 }
291 $cache->delete( $key );
292
293 return false;
294 } elseif ( $failed + $this->serviceFailurePeriod < wfTimestamp() ) {
295 /* We are in suspicious mode and one failure is enough to update
296 * failed timestamp. If the service works however, let's use it.
297 * Previous failures are forgotten after another failure period
298 * has passed */
299 return false;
300 }
301
302 // Check the failure count against the limit
303 return $count >= $this->serviceFailureCount;
304 }
305
307 protected function reportTranslationServiceFailure( string $msg ): void {
308 $service = $this->service;
309 $this->logger->warning( "Translation service $service problem: $msg" );
310
311 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
312 $key = $cache->makeKey( "translate-service-$service" );
313
314 $value = $cache->get( $key );
315 if ( !is_string( $value ) ) {
316 $count = 0;
317 } else {
318 list( $count, ) = explode( '|', $value, 2 );
319 }
320
321 $count++;
322 $failed = wfTimestamp();
323 $cache->set(
324 $key,
325 "$count|$failed",
326 $this->serviceFailurePeriod * 5
327 );
328
329 if ( $count === $this->serviceFailureCount ) {
330 $this->logger->error( "Translation service $service suspended" );
331 } elseif ( $count > $this->serviceFailureCount ) {
332 $this->logger->warning( "Translation service $service still suspended" );
333 }
334 }
335}
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), MessageIndex::singleton());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), new RevTagStore(), $services->getDBLoadBalancer());}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
Contains a class for querying external translation service.
Implements support for Microsoft translation api v3.
Implements support for cxserver proxied through RESTBase.
Value object that represents a HTTP(S) query response.
Mutable objects that represents a HTTP(S) query.
reportTranslationServiceFailure(string $msg)
Increases the failure count for this service.
getQueries(string $text, string $sourceLanguage, string $targetLanguage)
Get queries for this service.
isSupportedLanguagePair(string $sourceLanguage, string $targetLanguage)
Test whether given language pair is supported by the service.
mapCode(string $code)
Map a MediaWiki (almost standard) language code to the code used by the translation service.
getQuery(string $text, string $sourceLanguage, string $targetLanguage)
Get the query.
getResultData(TranslationQueryResponse $response)
Get the web service specific response returned by QueryAggregator.
doPairs()
Get the list of supported language pairs for the web service.
unwrapUntranslatable(string $text)
Undo the hopyfully untouched mangling done by wrapUntranslatable.
parseResponse(TranslationQueryResponse $response)
Get the response.
getName()
Gets the name of this service, for example to display it for the user.
wrapUntranslatable(string $text)
Some mangling that tries to keep some parts of the message unmangled by the translation service.
checkTranslationServiceFailure()
Checks whether the service has exceeded failure count.
static factory(string $serviceName, array $config)
Get a webservice handler.
Implements support for Yandex translation api v1.