Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ElasticSearchTtmServer.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\TtmServer;
5
6use Elastica\Aggregation\Terms;
7use Elastica\Client;
8use Elastica\Document;
9use Elastica\Exception\ExceptionInterface;
10use Elastica\Index;
11use Elastica\Mapping;
12use Elastica\Query;
13use Elastica\Query\BoolQuery;
14use Elastica\Query\FunctionScore;
15use Elastica\Query\MatchQuery;
16use Elastica\Query\Term;
17use Elastica\ResultSet;
18use Elastica\Search;
19use Exception;
20use MediaWiki\Extension\Elastica\MWElasticUtils;
24use MediaWiki\Logger\LoggerFactory;
25use MediaWiki\MediaWikiServices;
26use MediaWiki\Title\Title;
27use MediaWiki\WikiMap\WikiMap;
28use RuntimeException;
30
39 extends TtmServer
41{
47 private const BULK_INDEX_RETRY_ATTEMPTS = 5;
48
54 private const WAIT_UNTIL_READY_TIMEOUT = 3600;
55
56 private ?Client $client = null;
58 private ?TTMServerBootstrap $logger = null;
60 private bool $updateMapping = false;
61
62 public function isLocalSuggestion( array $suggestion ): bool {
63 return $suggestion['wiki'] === WikiMap::getCurrentWikiId();
64 }
65
66 public function expandLocation( array $suggestion ): string {
67 return $suggestion['uri'];
68 }
69
70 public function query( string $sourceLanguage, string $targetLanguage, string $text ): array {
71 try {
72 return $this->doQuery( $sourceLanguage, $targetLanguage, $text );
73 } catch ( Exception $e ) {
74 throw new TranslationHelperException( 'Elastica exception: ' . $e );
75 }
76 }
77
78 private function doQuery( string $sourceLanguage, string $targetLanguage, string $text ): array {
79 if ( !$this->useWikimediaExtraPlugin() ) {
80 // ElasticTTM is currently not compatible with elasticsearch 2.x/5.x
81 // It needs FuzzyLikeThis ported via the wmf extra plugin
82 throw new RuntimeException( 'The wikimedia extra plugin is mandatory.' );
83 }
84 /* Two query system:
85 * 1) Find all strings in source language that match text
86 * 2) Do another query for translations for those strings
87 */
88 $connection = $this->getClient()->getConnection();
89 $oldTimeout = $connection->getTimeout();
90 $connection->setTimeout( 10 );
91
92 $fuzzyQuery = new FuzzyLikeThis();
93 $fuzzyQuery->setLikeText( $text );
94 $fuzzyQuery->addFieldNames( [ 'content' ] );
95
96 $boostQuery = new FunctionScore();
97 $boostQuery->addFunction(
98 'levenshtein_distance_score',
99 [
100 'text' => $text,
101 'field' => 'content'
102 ]
103 );
104 $boostQuery->setBoostMode( FunctionScore::BOOST_MODE_REPLACE );
105
106 // Wrap the fuzzy query, so it can be used as a filter.
107 // This is slightly faster, as ES can throw away the scores by this query.
108 $bool = new BoolQuery();
109 $bool->addFilter( $fuzzyQuery );
110 $bool->addMust( $boostQuery );
111
112 $languageFilter = new Term();
113 $languageFilter->setTerm( 'language', $sourceLanguage );
114 $bool->addFilter( $languageFilter );
115
116 // The whole query
117 $query = new Query();
118 $query->setQuery( $bool );
119
120 // The interface usually displays three best candidates. These might
121 // come from more than three source things, if the translations are
122 // the same. In other words suggestions are grouped by the suggested
123 // translation. This algorithm might not find all suggestions, if the
124 // top N best matching source texts don't have equivalent translations
125 // in the target language, but worse matches which we did not fetch do.
126 // This code tries to balance between doing too many or too big queries
127 // and not fetching enough results to show all possible suggestions.
128 $sizeFirst = 100;
129 $sizeSecond = $sizeFirst * 5;
130
131 $query->setFrom( 0 );
132 $query->setSize( $sizeFirst );
133 $query->setParam( '_source', [ 'content' ] );
134 $cutoff = $this->config['cutoff'] ?? 0.65;
135 $query->setParam( 'min_score', $cutoff );
136 $query->setSort( [ '_score', 'wiki', 'localid' ] );
137
138 /* This query is doing two unrelated things:
139 * 1) Collect the message contents and scores so that they can
140 * be accessed later for the translations we found.
141 * 2) Build the query string for the query that fetches the translations.
142 */
143 $contents = $scores = $terms = [];
144 do {
145 $resultset = $this->getIndex()->search( $query );
146
147 if ( count( $resultset ) === 0 ) {
148 break;
149 }
150
151 foreach ( $resultset->getResults() as $result ) {
152 $data = $result->getData();
153 $score = $result->getScore();
154
155 $sourceId = preg_replace( '~/[^/]+$~', '', $result->getId() );
156 $contents[$sourceId] = $data['content'];
157 $scores[$sourceId] = $score;
158 $terms[] = "$sourceId/$targetLanguage";
159 }
160
161 // Check if it looks like that we are hitting the long tail already.
162 // Otherwise, we'll do a query to fetch some more to reach a "sane"
163 // breaking point, i.e. include all suggestions with same content
164 // for reliable used X times statistics.
165 if ( count( array_unique( $scores ) ) > 5 ) {
166 break;
167 }
168
169 // Okay, We are now in second iteration of the loop. We already got
170 // lots of suggestions. We will give up for now even if it means we
171 // return in some sense incomplete results.
172 if ( count( $resultset ) === $sizeSecond ) {
173 break;
174 }
175
176 // After the first query, the smallest score is the new threshold.
177 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable
178 $query->setParam( 'min_score', $score );
179 $query->setFrom( $query->getParam( 'size' ) + $query->getParam( 'from' ) );
180 $query->setSize( $sizeSecond );
181
182 // Break if we already got all hits
183 } while ( $resultset->getTotalHits() > count( $contents ) );
184
185 $suggestions = [];
186
187 // Skip second query if first query found nothing. Keeping only one return
188 // statement in this method to avoid forgetting to reset connection timeout
189 if ( $terms !== [] ) {
190 $idQuery = new Query\Terms( '_id', $terms );
191
192 $query = new Query( $idQuery );
193 $query->setSize( 25 );
194 $query->setParam( '_source', [ 'wiki', 'uri', 'content', 'localid' ] );
195 $resultset = $this->getIndex()->search( $query );
196
197 foreach ( $resultset->getResults() as $result ) {
198 $data = $result->getData();
199
200 // Construct the matching source id
201 $sourceId = preg_replace( '~/[^/]+$~', '', $result->getId() );
202
203 $suggestions[] = [
204 'source' => $contents[$sourceId],
205 'target' => $data['content'],
206 'context' => $data['localid'],
207 'quality' => $scores[$sourceId],
208 'wiki' => $data['wiki'],
209 'location' => $data['localid'] . '/' . $targetLanguage,
210 'uri' => $data['uri'],
211 ];
212 }
213
214 // Ensure results are in quality order
215 uasort( $suggestions, static function ( $a, $b ) {
216 if ( $a['quality'] === $b['quality'] ) {
217 return 0;
218 }
219
220 return ( $a['quality'] < $b['quality'] ) ? 1 : -1;
221 } );
222 }
223
224 $connection->setTimeout( $oldTimeout );
225
226 return $suggestions;
227 }
228
229 /* Write functions */
230
231 public function update( MessageHandle $handle, ?string $targetText ): bool {
232 if ( !$handle->isValid() || $handle->getCode() === '' ) {
233 return false;
234 }
235
236 /* There are various different cases here:
237 * [new or updated] [fuzzy|non-fuzzy] [translation|definition]
238 * 1) We don't distinguish between new or updated here.
239 * 2) Delete old translation, but not definition
240 * 3) Insert new translation or definition, if non-fuzzy
241 * The definition should never be fuzzied anyway.
242 *
243 * These only apply to known messages.
244 */
245
246 $sourceLanguage = $handle->getGroup()->getSourceLanguage();
247
248 // Do not delete definitions, because the translations are attached to that
249 if ( $handle->getCode() !== $sourceLanguage ) {
250 $localid = $handle->getTitleForBase()->getPrefixedText();
251 $this->deleteByQuery( $this->getIndex(), Query::create(
252 ( new BoolQuery() )
253 ->addFilter( new Term( [ 'wiki' => WikiMap::getCurrentWikiId() ] ) )
254 ->addFilter( new Term( [ 'language' => $handle->getCode() ] ) )
255 ->addFilter( new Term( [ 'localid' => $localid ] ) ) ) );
256 }
257
258 // If translation was made fuzzy, we do not need to add anything
259 if ( $targetText === null ) {
260 return true;
261 }
262
263 // source language is null, skip doing rest of the stuff
264 if ( $sourceLanguage === null ) {
265 return true;
266 }
267
268 $revId = $handle->getTitleForLanguage( $sourceLanguage )->getLatestRevID();
269 $doc = $this->createDocument( $handle, $targetText, $revId );
270 $fname = __METHOD__;
271
272 MWElasticUtils::withRetry( self::BULK_INDEX_RETRY_ATTEMPTS,
273 function () use ( $doc ) {
274 $this->getIndex()->addDocuments( [ $doc ] );
275 },
276 static function ( $e, $errors ) use ( $fname ) {
277 $c = get_class( $e );
278 $msg = $e->getMessage();
279 error_log( $fname . ": update failed ($c: $msg); retrying." );
280 sleep( 10 );
281 }
282 );
283
284 return true;
285 }
286
287 private function createDocument( MessageHandle $handle, string $text, int $revId ): Document {
288 $language = $handle->getCode();
289
290 $localid = $handle->getTitleForBase()->getPrefixedText();
291 $wiki = WikiMap::getCurrentWikiId();
292 $globalid = "$wiki-$localid-$revId/$language";
293
294 $data = [
295 'wiki' => $wiki,
296 'uri' => $handle->getTitle()->getCanonicalURL(),
297 'localid' => $localid,
298 'language' => $language,
299 'content' => $text,
300 'group' => $handle->getGroupIds(),
301 ];
302
303 return new Document( $globalid, $data );
304 }
305
307 private function createIndex( bool $rebuild ): void {
308 $indexSettings = [
309 'settings' => [
310 'index' => [
311 'number_of_shards' => $this->getShardCount(),
312 'analysis' => [
313 'filter' => [
314 'prefix_filter' => [
315 'type' => 'edge_ngram',
316 'min_gram' => 2,
317 'max_gram' => 20
318 ]
319 ],
320 'analyzer' => [
321 'prefix' => [
322 'type' => 'custom',
323 'tokenizer' => 'standard',
324 'filter' => [ 'lowercase', 'prefix_filter' ]
325 ],
326 'casesensitive' => [
327 'tokenizer' => 'standard'
328 ]
329 ]
330 ]
331 ],
332 ],
333 ];
334 $replicas = $this->getReplicaCount();
335 $key = is_string( $replicas ) && str_contains( $replicas, '-' )
336 ? 'auto_expand_replicas'
337 : 'number_of_replicas';
338 $indexSettings['settings']['index'][$key] = $replicas;
339
340 $this->getIndex()->create( $indexSettings, $rebuild );
341 }
342
347 public function beginBootstrap(): void {
348 $this->checkElasticsearchVersion();
349 $index = $this->getIndex();
350 if ( $this->updateMapping ) {
351 $this->logOutput( 'Updating the index mappings...' );
352 $this->createIndex( true );
353 } elseif ( !$index->exists() ) {
354 $this->createIndex( false );
355 }
356
357 $settings = $index->getSettings();
358 $settings->setRefreshInterval( '-1' );
359
360 $this->deleteByQuery( $this->getIndex(), Query::create(
361 ( new Term() )->setTerm( 'wiki', WikiMap::getCurrentWikiId() ) ) );
362
363 $properties = [
364 'wiki' => [ 'type' => 'keyword' ],
365 'localid' => [ 'type' => 'keyword' ],
366 'uri' => [ 'type' => 'keyword' ],
367 'language' => [ 'type' => 'keyword' ],
368 'group' => [ 'type' => 'keyword' ],
369 'content' => [
370 'type' => 'text',
371 'fields' => [
372 'content' => [
373 'type' => 'text',
374 'term_vector' => 'yes'
375 ],
376 'prefix_complete' => [
377 'type' => 'text',
378 'analyzer' => 'prefix',
379 'search_analyzer' => 'standard',
380 'term_vector' => 'yes'
381 ],
382 'case_sensitive' => [
383 'type' => 'text',
384 'analyzer' => 'casesensitive',
385 'term_vector' => 'yes'
386 ]
387 ]
388 ],
389 ];
390
391 $mapping = new Mapping( $properties );
392 $mapping->send( $index );
393
394 $this->waitUntilReady();
395 }
396
397 public function beginBatch(): void {
398 }
399
404 public function batchInsertDefinitions( array $batch ): void {
405 $lb = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch();
406 foreach ( $batch as $data ) {
407 $lb->addObj( $data[0]->getTitle() );
408 }
409 $lb->execute();
410
411 $this->batchInsertTranslations( $batch );
412 }
413
414 public function batchInsertTranslations( array $batch ): void {
415 $docs = [];
416 foreach ( $batch as [ $handle, $sourceLanguage, $text ] ) {
417 $revId = $handle->getTitleForLanguage( $sourceLanguage )->getLatestRevID();
418 $docs[] = $this->createDocument( $handle, $text, $revId );
419 }
420
421 MWElasticUtils::withRetry( self::BULK_INDEX_RETRY_ATTEMPTS,
422 function () use ( $docs ) {
423 $this->getIndex()->addDocuments( $docs );
424 },
425 function ( $e, $errors ) {
426 $c = get_class( $e );
427 $msg = $e->getMessage();
428 $this->logOutput( "Batch failed ($c: $msg), trying again in 10 seconds" );
429 sleep( 10 );
430 }
431 );
432 }
433
434 public function endBatch(): void {
435 }
436
437 public function endBootstrap(): void {
438 $index = $this->getIndex();
439 $index->refresh();
440 $index->forcemerge();
441 $index->getSettings()->setRefreshInterval( '5s' );
442 }
443
444 public function getClient(): Client {
445 if ( $this->client === null ) {
446 if ( isset( $this->config['config'] ) ) {
447 $this->client = new Client( $this->config['config'] );
448 } else {
449 $this->client = new Client();
450 }
451 }
452 return $this->client;
453 }
454
456 public function useWikimediaExtraPlugin(): bool {
457 return isset( $this->config['use_wikimedia_extra'] ) && $this->config['use_wikimedia_extra'];
458 }
459
460 private function getIndexName(): string {
461 return $this->config['index'] ?? 'ttmserver';
462 }
463
464 public function getIndex(): Index {
465 return $this->getClient()
466 ->getIndex( $this->getIndexName() );
467 }
468
469 private function getShardCount(): int {
470 return $this->config['shards'] ?? 1;
471 }
472
473 private function getReplicaCount(): int|string {
474 return $this->config['replicas'] ?? '0-2';
475 }
476
477 private function waitUntilReady(): void {
478 $statuses = MWElasticUtils::waitForGreen(
479 $this->getClient(),
480 $this->getIndexName(),
481 self::WAIT_UNTIL_READY_TIMEOUT );
482 $this->logOutput( "Waiting for the index to go green..." );
483 foreach ( $statuses as $message ) {
484 $this->logOutput( $message );
485 }
486
487 if ( !$statuses->getReturn() ) {
488 die( "Timeout! Please check server logs for {$this->getIndexName()}." );
489 }
490 }
491
492 public function setLogger( TTMServerBootstrap $logger ): void {
493 $this->logger = $logger;
494 }
495
496 // Can it get any uglier?
497 private function logOutput( string $text ): void {
498 if ( $this->logger !== null ) {
499 $this->logger->statusLine( "$text\n" );
500 }
501 }
502
503 public function setDoReIndex(): void {
504 $this->updateMapping = true;
505 }
506
508 private function parseQueryString( string $queryString, array $opts ): array {
509 $fields = $highlights = [];
510 $terms = preg_split( '/\s+/', $queryString );
511 $match = $opts['match'];
512 $case = $opts['case'];
513
514 // Map each word in the query string with its corresponding field
515 foreach ( $terms as $term ) {
516 $prefix = strstr( $term, '*', true );
517 if ( $prefix ) {
518 // For wildcard search
519 $fields['content.prefix_complete'][] = $prefix;
520 } elseif ( $case === '1' ) {
521 // For case-sensitive search
522 $fields['content.case_sensitive'][] = $term;
523 } else {
524 $fields['content'][] = $term;
525 }
526 }
527
528 // Allow searching either by message content or message id (page name
529 // without language subpage) with exact match only.
530 $searchQuery = new BoolQuery();
531 foreach ( $fields as $analyzer => $words ) {
532 foreach ( $words as $word ) {
533 $boolQuery = new BoolQuery();
534 $contentQuery = new MatchQuery();
535 $contentQuery->setFieldQuery( $analyzer, $word );
536 $boolQuery->addShould( $contentQuery );
537 $messageQuery = new Term();
538 $messageQuery->setTerm( 'localid', $word );
539 $boolQuery->addShould( $messageQuery );
540
541 if ( $match === 'all' ) {
542 $searchQuery->addMust( $boolQuery );
543 } else {
544 $searchQuery->addShould( $boolQuery );
545 }
546
547 // Fields for highlighting
548 $highlights[$analyzer] = [
549 'number_of_fragments' => 0
550 ];
551
552 // Allow searching by exact message title (page name with
553 // language subpage).
554 $title = Title::newFromText( $word );
555 if ( !$title ) {
556 continue;
557 }
558 $handle = new MessageHandle( $title );
559 if ( $handle->isValid() && $handle->getCode() !== '' ) {
560 $localid = $handle->getTitleForBase()->getPrefixedText();
561 $boolQuery = new BoolQuery();
562 $messageId = new Term();
563 $messageId->setTerm( 'localid', $localid );
564 $boolQuery->addMust( $messageId );
565 $searchQuery->addShould( $boolQuery );
566 }
567 }
568 }
569
570 return [ $searchQuery, $highlights ];
571 }
572
574 public function createSearch( string $queryString, array $opts, array $highlight ): Search {
575 $query = new Query();
576
577 [ $searchQuery, $highlights ] = $this->parseQueryString( $queryString, $opts );
578 $query->setQuery( $searchQuery );
579
580 $language = new Terms( 'language' );
581 $language->setField( 'language' );
582 $language->setSize( 500 );
583 $query->addAggregation( $language );
584
585 $group = new Terms( 'group' );
586 $group->setField( 'group' );
587 // Would like to prioritize the top level groups and not show subgroups
588 // if the top group has only few hits, but that doesn't seem to be possile.
589 $group->setSize( 500 );
590 $query->addAggregation( $group );
591
592 $query->setSize( $opts['limit'] );
593 $query->setFrom( $opts['offset'] );
594
595 // BoolAnd filters are executed in sequence per document. Bool filters with
596 // multiple must clauses are executed by converting each filter into a bit
597 // field then anding them together. The latter is normally faster if either
598 // of the subfilters are reused. May not make a difference in this context.
599 $filters = new BoolQuery();
600
601 $language = $opts['language'];
602 if ( $language !== '' ) {
603 $languageFilter = new Term();
604 $languageFilter->setTerm( 'language', $language );
605 $filters->addFilter( $languageFilter );
606 }
607
608 $group = $opts['group'];
609 if ( $group !== '' ) {
610 $groupFilter = new Term();
611 $groupFilter->setTerm( 'group', $group );
612 $filters->addFilter( $groupFilter );
613 }
614
615 // Check that we have at least one filter to avoid invalid query errors.
616 if ( $language !== '' || $group !== '' ) {
617 // TODO: This seems wrong, but perhaps for aggregation purposes?
618 // should make $search a must clause and use the bool query
619 // as main.
620 $query->setPostFilter( $filters );
621 }
622
623 [ $pre, $post ] = $highlight;
624 $query->setHighlight( [
625 // The value must be an object
626 'pre_tags' => [ $pre ],
627 'post_tags' => [ $post ],
628 'fields' => $highlights,
629 ] );
630
631 return $this->getIndex()->createSearch( $query );
632 }
633
638 public function search( string $queryString, array $opts, array $highlight ): ResultSet {
639 $search = $this->createSearch( $queryString, $opts, $highlight );
640
641 try {
642 return $search->search();
643 } catch ( ExceptionInterface $e ) {
644 throw new TtmServerException( $e->getMessage() );
645 }
646 }
647
649 public function getFacets( $resultset ): array {
650 $this->assertResultSetInstance( $resultset );
651 $aggs = $resultset->getAggregations();
652 '@phan-var array[][][] $aggs';
653
654 $ret = [
655 'language' => [],
656 'group' => []
657 ];
658
659 foreach ( $aggs as $type => $info ) {
660 foreach ( $info['buckets'] as $row ) {
661 $ret[$type][$row['key']] = $row['doc_count'];
662 }
663 }
664
665 return $ret;
666 }
667
669 public function getTotalHits( $resultset ): int {
670 $this->assertResultSetInstance( $resultset );
671 return $resultset->getTotalHits();
672 }
673
675 public function getDocuments( $resultset ): array {
676 $this->assertResultSetInstance( $resultset );
677 $ret = [];
678 foreach ( $resultset->getResults() as $document ) {
679 $data = $document->getData();
680 $hl = $document->getHighlights();
681 if ( isset( $hl['content.prefix_complete'][0] ) ) {
682 $data['content'] = $hl['content.prefix_complete'][0];
683 } elseif ( isset( $hl['content.case_sensitive'][0] ) ) {
684 $data['content'] = $hl['content.case_sensitive'][0];
685 } elseif ( isset( $hl['content'][0] ) ) {
686 $data['content'] = $hl['content'][0];
687 }
688 $ret[] = $data;
689 }
690
691 return $ret;
692 }
693
700 private function deleteByQuery( Index $sourceIndex, Query $query ): void {
701 try {
702 MWElasticUtils::deleteByQuery( $sourceIndex, $query, /* $allowConflicts = */ true );
703 } catch ( Exception $e ) {
704 LoggerFactory::getInstance( LogNames::ELASTIC_SEARCH_TTMSERVER )->error(
705 'Problem encountered during deletion.',
706 [ 'exception' => $e ]
707 );
708
709 throw new RuntimeException( "Problem encountered during deletion.\n" . $e );
710 }
711 }
712
713 /* @throws RuntimeException */
714 private function getElasticsearchVersion(): string {
715 $response = $this->getClient()->request( '' );
716 if ( !$response->isOK() ) {
717 throw new RuntimeException( "Cannot fetch elasticsearch version: " . $response->getError() );
718 }
719
720 $result = $response->getData();
721 if ( !isset( $result['version']['number'] ) ) {
722 throw new RuntimeException( 'Unable to determine elasticsearch version, aborting.' );
723 }
724
725 $distribution = $result['version']['distribution'] ?? 'elasticsearch';
726
727 return $distribution . '-' . $result[ 'version' ][ 'number' ];
728 }
729
730 private function checkElasticsearchVersion(): void {
731 $version = $this->getElasticsearchVersion();
732 if ( !str_starts_with( $version, 'elasticsearch-6.8' )
733 && !str_starts_with( $version, 'elasticsearch-7.' )
734 && !str_starts_with( $version, 'opensearch-1.' )
735 && !str_starts_with( $version, 'opensearch-2.' ) ) {
736 throw new RuntimeException(
737 'Only OpenSearch 1.x and 2.x, Elasticsearch 6.8.x and 7.x are supported. ' .
738 "Your version: $version." );
739 }
740 }
741
743 private function assertResultSetInstance( $resultSet ): void {
744 if ( $resultSet instanceof ResultSet ) {
745 return;
746 }
747
748 throw new RuntimeException(
749 "Expected resultset to be an instance of " . ResultSet::class
750 );
751 }
752}
753
754// Translation memory configuration ($wgTranslateTranslationServices) uses class as ElasticSearchTTMServer
755// See: https://www.mediawiki.org/wiki/Help:Extension:Translate/Translation_memories#Configuration
756class_alias( ElasticSearchTtmServer::class, "ElasticSearchTTMServer" );
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(), $services->getContentLanguageCode() ->toString(), 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 { if(! $services->getExtensionRegistry() ->isLoaded( 'Echo')) { return null;} 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(), $services->get( 'Translate:HookRunner'),);}, '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->getConnectionProvider());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);}, 'Translate:WorkflowStatesMessageGroupLoader'=> static function(MediaWikiServices $services):WorkflowStatesMessageGroupLoader { return new WorkflowStatesMessageGroupLoader(new ServiceOptions(WorkflowStatesMessageGroupLoader::CONSTRUCTOR_OPTIONS, $services->getMainConfig()),);},]
@phpcs-require-sorted-array
Constants for log channel names used in this extension.
Definition LogNames.php:13
Class for pointing to messages, like Title class is for titles.
Translation helpers can throw this exception when they cannot do anything useful with the current mes...
update(MessageHandle $handle, ?string $targetText)
Shovels the new translation into translation memory.
isLocalSuggestion(array $suggestion)
Determines if the suggestion returned by this TtmServer comes from this wiki or any other wiki.
search(string $queryString, array $opts, array $highlight)
Search interface.
batchInsertTranslations(array $batch)
Called multiple times per batch if necessary.
setDoReIndex()
Instruct the service to fully wipe the index and start from scratch.
query(string $sourceLanguage, string $targetLanguage, string $text)
Fetches all relevant suggestions for given text.
createSearch(string $queryString, array $opts, array $highlight)
Search interface.
expandLocation(array $suggestion)
Given suggestion returned by this TtmServer, constructs fully qualified URL to the location of the tr...
Class to handle TtmServer specific exceptions.
TtmServer - The Translate extension translation memory interface Some general static methods for inst...
Definition TtmServer.php:19
Script to bootstrap TtmServer translation memory.
Interface for TtmServer that can be queried (=all of them).
Interface for TtmServer that can act as backend for translation search.
Interface for TtmServer that can be updated.