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;
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 = ObjectCache::getInstance( CACHE_ANYTHING );
227
228 return $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
250 protected function wrapUntranslatable( string $text ): string {
251 $text = str_replace( "\n", '!N!', $text );
252 $pattern = '~%[^% ]+%|\$\d|{VAR:[^}]+}|{?{(PLURAL|GRAMMAR|GENDER):[^|]+\||%(\d\$)?[sd]~';
253 $wrap = '<span class="notranslate" translate="no">\0</span>';
254 return preg_replace( $pattern, $wrap, $text );
255 }
256
258 protected function unwrapUntranslatable( string $text ): string {
259 $text = str_replace( '!N!', "\n", $text );
260 $pattern = '~<span class="notranslate" translate="no">(.*?)</span>~';
261 return preg_replace( $pattern, '\1', $text );
262 }
263
264 /* Failure handling and suspending */
265
266 public function setLogger( LoggerInterface $logger ): void {
267 $this->logger = $logger;
268 }
269
274 protected $serviceFailureCount = 5;
279 protected $serviceFailurePeriod = 900;
280
282 public function checkTranslationServiceFailure(): bool {
283 $service = $this->service;
284 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
285
286 $key = $cache->makeKey( "translate-service-$service" );
287 $value = $cache->get( $key );
288 if ( !is_string( $value ) ) {
289 return false;
290 }
291
292 [ $count, $failed ] = explode( '|', $value, 2 );
293 $count = (int)$count;
294 $failed = (int)$failed;
295 $now = (int)wfTimestamp();
296
297 if ( $failed + ( 2 * $this->serviceFailurePeriod ) < $now ) {
298 if ( $count >= $this->serviceFailureCount ) {
299 $this->logger->warning( "Translation service $service (was) restored" );
300 }
301 $cache->delete( $key );
302
303 return false;
304 } elseif ( $failed + $this->serviceFailurePeriod < $now ) {
305 /* We are in suspicious mode and one failure is enough to update
306 * failed timestamp. If the service works however, let's use it.
307 * Previous failures are forgotten after another failure period
308 * has passed */
309 return false;
310 }
311
312 // Check the failure count against the limit
313 return $count >= $this->serviceFailureCount;
314 }
315
317 protected function reportTranslationServiceFailure( string $msg ): void {
318 $service = $this->service;
319 $this->logger->warning( "Translation service $service problem: $msg" );
320
321 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
322 $key = $cache->makeKey( "translate-service-$service" );
323
324 $value = $cache->get( $key );
325 if ( !is_string( $value ) ) {
326 $count = 0;
327 } else {
328 [ $count, ] = explode( '|', $value, 2 );
329 }
330
331 $count++;
332 $failed = wfTimestamp();
333 $cache->set(
334 $key,
335 "$count|$failed",
336 $this->serviceFailurePeriod * 5
337 );
338
339 if ( $count === $this->serviceFailureCount ) {
340 $this->logger->error( "Translation service $service suspended" );
341 } elseif ( $count > $this->serviceFailureCount ) {
342 $this->logger->warning( "Translation service $service still suspended" );
343 }
344 }
345}
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager( $services->getTitleFactory());}, '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( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), 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: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->getDBLoadBalancer());}, '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->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( 'Translate.MessageGroupSubscription'), 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->getDBLoadBalancerFactory());}, '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( 'Translate'), $services->getMainObjectStash(), $services->getDBLoadBalancerFactory(), $services->get( 'Translate:MessageGroupSubscription'), 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->getDBLoadBalancer(), $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->getDBLoadBalancer());}, '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->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(), $services->getNamespaceInfo(), $services->getTitleFactory());}, '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->getDBLoadBalancerFactory(), $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:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getDBLoadBalancer(), $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'));}, '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->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getDBLoadBalancerFactory(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getDBLoadBalancer(), $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 { $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.
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.