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' => ApertiumCxserverWebService::class,
54 'deps' => [ 'HttpRequestFactory' ]
55 ],
56 'restbase' => [
57 'class' => RESTBaseWebService::class,
58 'deps' => [ 'HttpRequestFactory' ]
59 ],
60 'caighdean' => [
61 'class' => CaighdeanWebService::class
62 ],
63 'mint' => [
64 'class' => MintCxserverWebService::class,
65 'deps' => [ 'HttpRequestFactory' ]
66 ]
67 ];
68
69 if ( !isset( $config['timeout'] ) ) {
70 $config['timeout'] = 3;
71 }
72
73 $serviceDetails = $handlers[$config['type']] ?? null;
74 if ( $serviceDetails ) {
75 $objectFactory = MediaWikiServices::getInstance()->getObjectFactory();
76 $spec = [
77 'class' => $serviceDetails['class'],
78 'args' => [ $serviceName, $config ],
79 'services' => $serviceDetails['deps'] ?? [],
80 ];
81
82 // @phan-suppress-next-line PhanTypeInvalidCallableArraySize due to annotations on createObject?
83 $serviceObject = $objectFactory->createObject( $spec );
84 if ( $serviceObject instanceof LoggerAwareInterface ) {
85 $serviceObject->setLogger( LoggerFactory::getInstance( 'translationservices' ) );
86 }
87
88 return $serviceObject;
89 }
90
91 return null;
92 }
93
98 public function getName(): string {
99 return $this->service;
100 }
101
109 public function getQueries( string $text, string $sourceLanguage, string $targetLanguage ): array {
110 $from = $this->mapCode( $sourceLanguage );
111 $to = $this->mapCode( $targetLanguage );
112
113 try {
114 return [ $this->getQuery( $text, $from, $to ) ];
115 } catch ( TranslationWebServiceException $e ) {
116 $this->reportTranslationServiceFailure( $e->getMessage() );
117 return [];
118 } catch ( TranslationWebServiceInvalidInputException $e ) {
119 // Not much we can do about this, just ignore.
120 return [];
121 }
122 }
123
129 public function getResultData( TranslationQueryResponse $response ) {
130 if ( $response->getStatusCode() !== 200 ) {
131 $this->reportTranslationServiceFailure(
132 'STATUS: ' . $response->getStatusMessage() . "\n" .
133 'BODY: ' . $response->getBody()
134 );
135 return null;
136 }
137
138 try {
139 return $this->parseResponse( $response );
140 } catch ( TranslationWebServiceException $e ) {
141 $this->reportTranslationServiceFailure( $e->getMessage() );
142 return null;
143 }
144 }
145
150 abstract public function getType(): string;
151
152 /* Service api */
153
158 abstract protected function mapCode( string $code ): string;
159
168 abstract protected function doPairs(): array;
169
180 abstract protected function getQuery(
181 string $text, string $sourceLanguage, string $targetLanguage
183
189 abstract protected function parseResponse( TranslationQueryResponse $response );
190
191 /* Default implementation */
192
194 protected $service;
196 protected $config;
198 protected $logger;
199
200 public function __construct( string $service, array $config ) {
201 $this->service = $service;
202 $this->config = $config;
203 }
204
210 public function isSupportedLanguagePair( string $sourceLanguage, string $targetLanguage ): bool {
211 $pairs = $this->getSupportedLanguagePairs();
212 $from = $this->mapCode( $sourceLanguage );
213 $to = $this->mapCode( $targetLanguage );
214
215 return isset( $pairs[$from][$to] );
216 }
217
222 protected function getSupportedLanguagePairs(): array {
223 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
224
225 return $cache->getWithSetCallback(
226 $cache->makeKey( 'translate-tmsug-pairs-' . $this->service ),
227 $cache::TTL_DAY,
228 function ( &$ttl ) use ( $cache ) {
229 try {
230 $pairs = $this->doPairs();
231 } catch ( Exception $e ) {
232 $pairs = [];
233 $this->reportTranslationServiceFailure( $e->getMessage() );
234 $ttl = $cache::TTL_UNCACHEABLE;
235 }
236
237 return $pairs;
238 }
239 );
240 }
241
247 protected function wrapUntranslatable( string $text ): string {
248 $text = str_replace( "\n", '!N!', $text );
249 $pattern = '~%[^% ]+%|\$\d|{VAR:[^}]+}|{?{(PLURAL|GRAMMAR|GENDER):[^|]+\||%(\d\$)?[sd]~';
250 $wrap = '<span class="notranslate" translate="no">\0</span>';
251 return preg_replace( $pattern, $wrap, $text );
252 }
253
255 protected function unwrapUntranslatable( string $text ): string {
256 $text = str_replace( '!N!', "\n", $text );
257 $pattern = '~<span class="notranslate" translate="no">(.*?)</span>~';
258 return preg_replace( $pattern, '\1', $text );
259 }
260
261 /* Failure handling and suspending */
262
263 public function setLogger( LoggerInterface $logger ): void {
264 $this->logger = $logger;
265 }
266
271 protected $serviceFailureCount = 5;
276 protected $serviceFailurePeriod = 900;
277
279 public function checkTranslationServiceFailure(): bool {
280 $service = $this->service;
281 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
282
283 $key = $cache->makeKey( "translate-service-$service" );
284 $value = $cache->get( $key );
285 if ( !is_string( $value ) ) {
286 return false;
287 }
288
289 [ $count, $failed ] = explode( '|', $value, 2 );
290 $count = (int)$count;
291 $failed = (int)$failed;
292 $now = (int)wfTimestamp();
293
294 if ( $failed + ( 2 * $this->serviceFailurePeriod ) < $now ) {
295 if ( $count >= $this->serviceFailureCount ) {
296 $this->logger->warning( "Translation service $service (was) restored" );
297 }
298 $cache->delete( $key );
299
300 return false;
301 } elseif ( $failed + $this->serviceFailurePeriod < $now ) {
302 /* We are in suspicious mode and one failure is enough to update
303 * failed timestamp. If the service works however, let's use it.
304 * Previous failures are forgotten after another failure period
305 * has passed */
306 return false;
307 }
308
309 // Check the failure count against the limit
310 return $count >= $this->serviceFailureCount;
311 }
312
314 protected function reportTranslationServiceFailure( string $msg ): void {
315 $service = $this->service;
316 $this->logger->warning( "Translation service $service problem: $msg" );
317
318 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
319 $key = $cache->makeKey( "translate-service-$service" );
320
321 $value = $cache->get( $key );
322 if ( !is_string( $value ) ) {
323 $count = 0;
324 } else {
325 [ $count, ] = explode( '|', $value, 2 );
326 }
327
328 $count++;
329 $failed = wfTimestamp();
330 $cache->set(
331 $key,
332 "$count|$failed",
333 $this->serviceFailurePeriod * 5
334 );
335
336 if ( $count === $this->serviceFailureCount ) {
337 $this->logger->error( "Translation service $service suspended" );
338 } elseif ( $count > $this->serviceFailureCount ) {
339 $this->logger->warning( "Translation service $service still suspended" );
340 }
341 }
342}
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'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, '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:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, '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:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, '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:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, '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(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(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(), $services->getDBLoadBalancer());}, '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
Implements support for Apertium translation service via the Cxserver API.
Implements support for Microsoft translation api v3.
Implements support for MinT translation service via the Cxserver API.
Implements support for cxserver proxied through RESTBase.
Value object that represents a HTTP(S) query response.
Mutable objects that represents an 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.