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;
8use MediaWiki\Logger\LoggerFactory;
9use MediaWiki\MediaWikiServices;
10use Psr\Log\LoggerAwareInterface;
11use Psr\Log\LoggerInterface;
12use Wikimedia\ObjectCache\BagOStuff;
13
25abstract class TranslationWebService implements LoggerAwareInterface {
26 private ?BagOStuff $cache;
27 private ?array $supportedLanguagePairs = null;
28
29 /* Public api */
30
35 public static function factory( string $serviceName, array $config ): ?TranslationWebService {
36 $handlers = [
37 'microsoft' => [
38 'class' => MicrosoftWebService::class,
39 'deps' => [ 'HttpRequestFactory' ]
40 ],
41 'apertium' => [
42 'class' => ApertiumWebService::class,
43 'deps' => [ 'HttpRequestFactory' ]
44 ],
45 'yandex' => [
46 'class' => YandexWebService::class,
47 'deps' => [ 'HttpRequestFactory' ]
48 ],
49 'google' => [
50 'class' => GoogleTranslateWebService::class,
51 'deps' => [ 'HttpRequestFactory' ]
52 ],
53 'remote-ttmserver' => [
54 'class' => RemoteTTMServerWebService::class
55 ],
56 'cxserver' => [
57 'class' => ApertiumCxserverWebService::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( LogNames::TRANSLATION_SERVICES ) );
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;
144 // Not much we can do about this, just ignore.
145 return null;
146 }
147 }
148
153 abstract public function getType(): string;
154
155 /* Service api */
156
161 abstract protected function mapCode( string $code ): string;
162
171 abstract protected function doPairs(): array;
172
183 abstract protected function getQuery(
184 string $text, string $sourceLanguage, string $targetLanguage
186
192 abstract protected function parseResponse( TranslationQueryResponse $response );
193
194 /* Default implementation */
195
197 protected $service;
199 protected $config;
201 protected $logger;
202
203 public function __construct( string $service, array $config ) {
204 $this->service = $service;
205 $this->config = $config;
206 }
207
213 public function isSupportedLanguagePair( string $sourceLanguage, string $targetLanguage ): bool {
214 $pairs = $this->getSupportedLanguagePairs();
215 $from = $this->mapCode( $sourceLanguage );
216 $to = $this->mapCode( $targetLanguage );
217
218 return isset( $pairs[$from][$to] );
219 }
220
225 protected function getSupportedLanguagePairs(): array {
226 $cache = $this->getObjectCache();
227
228 $this->supportedLanguagePairs ??= $cache->getWithSetCallback(
229 $cache->makeKey( 'translate-tmsug-pairs-' . $this->service ),
230 $cache::TTL_DAY,
231 function ( &$ttl ) use ( $cache ) {
232 try {
233 $pairs = $this->doPairs();
234 } catch ( Exception $e ) {
235 $pairs = [];
236 $this->reportTranslationServiceFailure( $e->getMessage() );
237 $ttl = $cache::TTL_UNCACHEABLE;
238 }
239
240 return $pairs;
241 }
242 );
243
244 return $this->supportedLanguagePairs;
245 }
246
252 protected function wrapUntranslatable( string $text ): string {
253 $text = str_replace( "\n", '!N!', $text );
254 $pattern = '~%[^% ]+%|\$\d|{VAR:[^}]+}|{?{(PLURAL|GRAMMAR|GENDER):[^|]+\||%(\d\$)?[sd]~';
255 $wrap = '<span class="notranslate" translate="no">\0</span>';
256 return preg_replace( $pattern, $wrap, $text );
257 }
258
260 protected function unwrapUntranslatable( string $text ): string {
261 $text = str_replace( '!N!', "\n", $text );
262 $pattern = '~<span class="notranslate" translate="no">(.*?)</span>~';
263 return preg_replace( $pattern, '\1', $text );
264 }
265
266 /* Failure handling and suspending */
267
268 public function setLogger( LoggerInterface $logger ): void {
269 $this->logger = $logger;
270 }
271
276 protected $serviceFailureCount = 5;
281 protected $serviceFailurePeriod = 900;
282
284 public function checkTranslationServiceFailure(): bool {
285 $service = $this->service;
286 $cache = $this->getObjectCache();
287
288 $key = $cache->makeKey( "translate-service-$service" );
289 $value = $cache->get( $key );
290 if ( !is_string( $value ) ) {
291 return false;
292 }
293
294 [ $count, $failed ] = explode( '|', $value, 2 );
295 $count = (int)$count;
296 $failed = (int)$failed;
297 $now = (int)wfTimestamp();
298
299 if ( $failed + ( 2 * $this->serviceFailurePeriod ) < $now ) {
300 if ( $count >= $this->serviceFailureCount ) {
301 $this->logger->warning( "Translation service $service (was) restored" );
302 }
303 $cache->delete( $key );
304
305 return false;
306 } elseif ( $failed + $this->serviceFailurePeriod < $now ) {
307 /* We are in suspicious mode and one failure is enough to update
308 * failed timestamp. If the service works however, let's use it.
309 * Previous failures are forgotten after another failure period
310 * has passed */
311 return false;
312 }
313
314 // Check the failure count against the limit
315 return $count >= $this->serviceFailureCount;
316 }
317
319 protected function reportTranslationServiceFailure( string $msg ): void {
320 $service = $this->service;
321 $this->logger->warning( "Translation service $service problem: $msg" );
322
323 $cache = $this->getObjectCache();
324 $key = $cache->makeKey( "translate-service-$service" );
325
326 $value = $cache->get( $key );
327 if ( !is_string( $value ) ) {
328 $count = 0;
329 } else {
330 [ $count, ] = explode( '|', $value, 2 );
331 }
332
333 $count++;
334 $failed = wfTimestamp();
335 $cache->set(
336 $key,
337 "$count|$failed",
338 $this->serviceFailurePeriod * 5
339 );
340
341 if ( $count === $this->serviceFailureCount ) {
342 $this->logger->error( "Translation service $service suspended" );
343 } elseif ( $count > $this->serviceFailureCount ) {
344 $this->logger->warning( "Translation service $service still suspended" );
345 }
346 }
347
348 private function getObjectCache(): BagOStuff {
349 $this->cache ??= MediaWikiServices::getInstance()
350 ->getObjectCacheFactory()
351 ->getInstance( CACHE_ANYTHING );
352
353 return $this->cache;
354 }
355}
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager($services->getTitleFactory(), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, '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:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance(LogNames::GROUP_SYNCHRONIZATION), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleDependencyPurger'=> static function(MediaWikiServices $services):MessageBundleDependencyPurger { return new MessageBundleDependencyPurger( $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getConnectionProvider());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getConnectionProvider(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):MessageGroupSubscriptionHookHandler { return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance(LogNames::MAIN), $services->getMainObjectStash(), $services->getConnectionProvider(), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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->getConnectionProvider(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getConnectionProvider());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getConnectionProvider());}, '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(), $services->getNamespaceInfo(), $services->getTitleFactory(), $services->getFormatterFactory());}, '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->getConnectionProvider(), $services->getObjectCacheFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getConnectionProvider() ->getPrimaryDatabase(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getConnectionProvider(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'), $services->get( 'Translate:MessageGroupSubscription'), $services->getFormatterFactory());}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getConnectionProvider(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getConnectionProvider(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getConnectionProvider(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { return new TranslationStashStorage( $services->getConnectionProvider() ->getPrimaryDatabase());}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getConnectionProvider());}, '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
Constants for log channel names used in this extension.
Definition LogNames.php:13
const TRANSLATION_SERVICES
Channel for translation services.
Definition LogNames.php:21
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.
Value object that represents a HTTP(S) query response.
Mutable objects that represents an HTTP(S) query.
Used to signal that the requested input is rejected and cannot be used with an external web service.
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.