47 private const BULK_INDEX_RETRY_ATTEMPTS = 5;
54 private const WAIT_UNTIL_READY_TIMEOUT = 3600;
56 private ?Client $client =
null;
60 private bool $updateMapping =
false;
63 return $suggestion[
'wiki'] === WikiMap::getCurrentWikiId();
70 public function query(
string $sourceLanguage,
string $targetLanguage,
string $text ): array {
72 return $this->doQuery( $sourceLanguage, $targetLanguage, $text );
73 }
catch ( Exception $e ) {
78 private function doQuery(
string $sourceLanguage,
string $targetLanguage,
string $text ): array {
79 if ( !$this->useWikimediaExtraPlugin() ) {
82 throw new RuntimeException(
'The wikimedia extra plugin is mandatory.' );
88 $connection = $this->getClient()->getConnection();
89 $oldTimeout = $connection->getTimeout();
90 $connection->setTimeout( 10 );
92 $fuzzyQuery =
new FuzzyLikeThis();
93 $fuzzyQuery->setLikeText( $text );
94 $fuzzyQuery->addFieldNames( [
'content' ] );
96 $boostQuery =
new FunctionScore();
97 $boostQuery->addFunction(
98 'levenshtein_distance_score',
104 $boostQuery->setBoostMode( FunctionScore::BOOST_MODE_REPLACE );
108 $bool =
new BoolQuery();
109 $bool->addFilter( $fuzzyQuery );
110 $bool->addMust( $boostQuery );
112 $languageFilter =
new Term();
113 $languageFilter->setTerm(
'language', $sourceLanguage );
114 $bool->addFilter( $languageFilter );
117 $query =
new Query();
118 $query->setQuery( $bool );
129 $sizeSecond = $sizeFirst * 5;
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' ] );
143 $contents = $scores = $terms = [];
145 $resultset = $this->getIndex()->search( $query );
147 if ( count( $resultset ) === 0 ) {
151 foreach ( $resultset->getResults() as $result ) {
152 $data = $result->getData();
153 $score = $result->getScore();
155 $sourceId = preg_replace(
'~/[^/]+$~',
'', $result->getId() );
156 $contents[$sourceId] = $data[
'content'];
157 $scores[$sourceId] = $score;
158 $terms[] =
"$sourceId/$targetLanguage";
165 if ( count( array_unique( $scores ) ) > 5 ) {
172 if ( count( $resultset ) === $sizeSecond ) {
178 $query->setParam(
'min_score', $score );
179 $query->setFrom( $query->getParam(
'size' ) + $query->getParam(
'from' ) );
180 $query->setSize( $sizeSecond );
183 }
while ( $resultset->getTotalHits() > count( $contents ) );
189 if ( $terms !== [] ) {
190 $idQuery =
new Query\Terms(
'_id', $terms );
192 $query =
new Query( $idQuery );
193 $query->setSize( 25 );
194 $query->setParam(
'_source', [
'wiki',
'uri',
'content',
'localid' ] );
195 $resultset = $this->getIndex()->search( $query );
197 foreach ( $resultset->getResults() as $result ) {
198 $data = $result->getData();
201 $sourceId = preg_replace(
'~/[^/]+$~',
'', $result->getId() );
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'],
215 uasort( $suggestions,
static function ( $a, $b ) {
216 if ( $a[
'quality'] === $b[
'quality'] ) {
220 return ( $a[
'quality'] < $b[
'quality'] ) ? 1 : -1;
224 $connection->setTimeout( $oldTimeout );
232 if ( !$handle->isValid() || $handle->getCode() ===
'' ) {
246 $sourceLanguage = $handle->getGroup()->getSourceLanguage();
249 if ( $handle->getCode() !== $sourceLanguage ) {
250 $localid = $handle->getTitleForBase()->getPrefixedText();
251 $this->deleteByQuery( $this->getIndex(), Query::create(
253 ->addFilter(
new Term( [
'wiki' => WikiMap::getCurrentWikiId() ] ) )
254 ->addFilter(
new Term( [
'language' => $handle->getCode() ] ) )
255 ->addFilter(
new Term( [
'localid' => $localid ] ) ) ) );
259 if ( $targetText ===
null ) {
264 if ( $sourceLanguage ===
null ) {
268 $revId = $handle->getTitleForLanguage( $sourceLanguage )->getLatestRevID();
269 $doc = $this->createDocument( $handle, $targetText, $revId );
272 MWElasticUtils::withRetry( self::BULK_INDEX_RETRY_ATTEMPTS,
273 function () use ( $doc ) {
274 $this->getIndex()->addDocuments( [ $doc ] );
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." );
287 private function createDocument( MessageHandle $handle,
string $text,
int $revId ): Document {
288 $language = $handle->getCode();
290 $localid = $handle->getTitleForBase()->getPrefixedText();
291 $wiki = WikiMap::getCurrentWikiId();
292 $globalid =
"$wiki-$localid-$revId/$language";
296 'uri' => $handle->getTitle()->getCanonicalURL(),
297 'localid' => $localid,
298 'language' => $language,
300 'group' => $handle->getGroupIds(),
303 return new Document( $globalid, $data );
307 private function createIndex(
bool $rebuild ): void {
311 'number_of_shards' => $this->getShardCount(),
315 'type' =>
'edge_ngram',
323 'tokenizer' =>
'standard',
324 'filter' => [
'lowercase',
'prefix_filter' ]
327 'tokenizer' =>
'standard'
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;
340 $this->getIndex()->create( $indexSettings, $rebuild );
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 );
357 $settings = $index->getSettings();
358 $settings->setRefreshInterval(
'-1' );
360 $this->deleteByQuery( $this->getIndex(), Query::create(
361 (
new Term() )->setTerm(
'wiki', WikiMap::getCurrentWikiId() ) ) );
364 'wiki' => [
'type' =>
'keyword' ],
365 'localid' => [
'type' =>
'keyword' ],
366 'uri' => [
'type' =>
'keyword' ],
367 'language' => [
'type' =>
'keyword' ],
368 'group' => [
'type' =>
'keyword' ],
374 'term_vector' =>
'yes'
376 'prefix_complete' => [
378 'analyzer' =>
'prefix',
379 'search_analyzer' =>
'standard',
380 'term_vector' =>
'yes'
382 'case_sensitive' => [
384 'analyzer' =>
'casesensitive',
385 'term_vector' =>
'yes'
391 $mapping =
new Mapping( $properties );
392 $mapping->send( $index );
394 $this->waitUntilReady();
404 public function batchInsertDefinitions( array $batch ): void {
405 $lb = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch();
406 foreach ( $batch as $data ) {
407 $lb->addObj( $data[0]->getTitle() );
411 $this->batchInsertTranslations( $batch );
416 foreach ( $batch as [ $handle, $sourceLanguage, $text ] ) {
417 $revId = $handle->getTitleForLanguage( $sourceLanguage )->getLatestRevID();
418 $docs[] = $this->createDocument( $handle, $text, $revId );
421 MWElasticUtils::withRetry( self::BULK_INDEX_RETRY_ATTEMPTS,
422 function () use ( $docs ) {
423 $this->getIndex()->addDocuments( $docs );
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" );
437 public function endBootstrap(): void {
438 $index = $this->getIndex();
440 $index->forcemerge();
441 $index->getSettings()->setRefreshInterval(
'5s' );
444 public function getClient(): Client {
445 if ( $this->client === null ) {
446 if ( isset( $this->config[
'config'] ) ) {
447 $this->client =
new Client( $this->config[
'config'] );
449 $this->client =
new Client();
452 return $this->client;
457 return isset( $this->config[
'use_wikimedia_extra'] ) && $this->config[
'use_wikimedia_extra'];
460 private function getIndexName(): string {
461 return $this->config[
'index'] ??
'ttmserver';
464 public function getIndex(): Index {
466 ->getIndex( $this->getIndexName() );
469 private function getShardCount(): int {
470 return $this->config[
'shards'] ?? 1;
473 private function getReplicaCount(): int|string {
474 return $this->config[
'replicas'] ??
'0-2';
477 private function waitUntilReady(): void {
478 $statuses = MWElasticUtils::waitForGreen(
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 );
487 if ( !$statuses->getReturn() ) {
488 die(
"Timeout! Please check server logs for {$this->getIndexName()}." );
493 $this->logger = $logger;
497 private function logOutput(
string $text ): void {
498 if ( $this->logger !== null ) {
499 $this->logger->statusLine(
"$text\n" );
504 $this->updateMapping = true;
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'];
515 foreach ( $terms as $term ) {
516 $prefix = strstr( $term,
'*',
true );
519 $fields[
'content.prefix_complete'][] = $prefix;
520 } elseif ( $case ===
'1' ) {
522 $fields[
'content.case_sensitive'][] = $term;
524 $fields[
'content'][] = $term;
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 );
541 if ( $match ===
'all' ) {
542 $searchQuery->addMust( $boolQuery );
544 $searchQuery->addShould( $boolQuery );
548 $highlights[$analyzer] = [
549 'number_of_fragments' => 0
554 $title = Title::newFromText( $word );
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 );
570 return [ $searchQuery, $highlights ];
574 public function createSearch(
string $queryString, array $opts, array $highlight ): Search {
575 $query = new Query();
577 [ $searchQuery, $highlights ] = $this->parseQueryString( $queryString, $opts );
578 $query->setQuery( $searchQuery );
580 $language =
new Terms(
'language' );
581 $language->setField(
'language' );
582 $language->setSize( 500 );
583 $query->addAggregation( $language );
585 $group =
new Terms(
'group' );
586 $group->setField(
'group' );
589 $group->setSize( 500 );
590 $query->addAggregation( $group );
592 $query->setSize( $opts[
'limit'] );
593 $query->setFrom( $opts[
'offset'] );
599 $filters =
new BoolQuery();
601 $language = $opts[
'language'];
602 if ( $language !==
'' ) {
603 $languageFilter =
new Term();
604 $languageFilter->setTerm(
'language', $language );
605 $filters->addFilter( $languageFilter );
608 $group = $opts[
'group'];
609 if ( $group !==
'' ) {
610 $groupFilter =
new Term();
611 $groupFilter->setTerm(
'group', $group );
612 $filters->addFilter( $groupFilter );
616 if ( $language !==
'' || $group !==
'' ) {
620 $query->setPostFilter( $filters );
623 [ $pre, $post ] = $highlight;
624 $query->setHighlight( [
626 'pre_tags' => [ $pre ],
627 'post_tags' => [ $post ],
628 'fields' => $highlights,
631 return $this->getIndex()->createSearch( $query );
638 public function search(
string $queryString, array $opts, array $highlight ): ResultSet {
639 $search = $this->createSearch( $queryString, $opts, $highlight );
642 return $search->search();
643 }
catch ( ExceptionInterface $e ) {
650 $this->assertResultSetInstance( $resultset );
651 $aggs = $resultset->getAggregations();
652 '@phan-var array[][][] $aggs';
659 foreach ( $aggs as $type => $info ) {
660 foreach ( $info[
'buckets'] as $row ) {
661 $ret[$type][$row[
'key']] = $row[
'doc_count'];
670 $this->assertResultSetInstance( $resultset );
671 return $resultset->getTotalHits();
676 $this->assertResultSetInstance( $resultset );
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];
700 private function deleteByQuery( Index $sourceIndex, Query $query ): void {
702 MWElasticUtils::deleteByQuery( $sourceIndex, $query, true );
703 }
catch ( Exception $e ) {
704 LoggerFactory::getInstance( LogNames::ELASTIC_SEARCH_TTMSERVER )->error(
705 'Problem encountered during deletion.',
706 [
'exception' => $e ]
709 throw new RuntimeException(
"Problem encountered during deletion.\n" . $e );
714 private function getElasticsearchVersion(): string {
715 $response = $this->getClient()->request(
'' );
716 if ( !$response->isOK() ) {
717 throw new RuntimeException(
"Cannot fetch elasticsearch version: " . $response->getError() );
720 $result = $response->getData();
721 if ( !isset( $result[
'version'][
'number'] ) ) {
722 throw new RuntimeException(
'Unable to determine elasticsearch version, aborting.' );
725 $distribution = $result[
'version'][
'distribution'] ??
'elasticsearch';
727 return $distribution .
'-' . $result[
'version' ][
'number' ];
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." );
743 private function assertResultSetInstance( $resultSet ): void {
744 if ( $resultSet instanceof ResultSet ) {
748 throw new RuntimeException(
749 "Expected resultset to be an instance of " . ResultSet::class
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