47 private const BULK_INDEX_RETRY_ATTEMPTS = 5;
54 private const WAIT_UNTIL_READY_TIMEOUT = 3600;
68 return $suggestion[
'wiki'] === WikiMap::getCurrentWikiId();
75 public function query(
string $sourceLanguage,
string $targetLanguage,
string $text ): array {
77 return $this->doQuery( $sourceLanguage, $targetLanguage, $text );
78 }
catch ( Exception $e ) {
83 protected function doQuery( $sourceLanguage, $targetLanguage, $text ) {
84 if ( !$this->useWikimediaExtraPlugin() ) {
87 throw new RuntimeException(
'The wikimedia extra plugin is mandatory.' );
93 $connection = $this->getClient()->getConnection();
94 $oldTimeout = $connection->getTimeout();
95 $connection->setTimeout( 10 );
98 $fuzzyQuery->setLikeText( $text );
99 $fuzzyQuery->addFields( [
'content' ] );
101 $boostQuery =
new FunctionScore();
102 $boostQuery->addFunction(
103 'levenshtein_distance_score',
109 $boostQuery->setBoostMode( FunctionScore::BOOST_MODE_REPLACE );
113 $bool =
new BoolQuery();
114 $bool->addFilter( $fuzzyQuery );
115 $bool->addMust( $boostQuery );
117 $languageFilter =
new Term();
118 $languageFilter->setTerm(
'language', $sourceLanguage );
119 $bool->addFilter( $languageFilter );
122 $query =
new Query();
123 $query->setQuery( $bool );
134 $sizeSecond = $sizeFirst * 5;
136 $query->setFrom( 0 );
137 $query->setSize( $sizeFirst );
138 $query->setParam(
'_source', [
'content' ] );
139 $cutoff = $this->config[
'cutoff'] ?? 0.65;
140 $query->setParam(
'min_score', $cutoff );
141 $query->setSort( [
'_score',
'wiki',
'localid' ] );
148 $contents = $scores = $terms = [];
150 $resultset = $this->getIndex()->search( $query );
152 if ( count( $resultset ) === 0 ) {
156 foreach ( $resultset->getResults() as $result ) {
157 $data = $result->getData();
158 $score = $result->getScore();
160 $sourceId = preg_replace(
'~/[^/]+$~',
'', $result->getId() );
161 $contents[$sourceId] = $data[
'content'];
162 $scores[$sourceId] = $score;
163 $terms[] =
"$sourceId/$targetLanguage";
170 if ( count( array_unique( $scores ) ) > 5 ) {
177 if ( count( $resultset ) === $sizeSecond ) {
183 $query->setParam(
'min_score', $score );
184 $query->setFrom( $query->getParam(
'size' ) + $query->getParam(
'from' ) );
185 $query->setSize( $sizeSecond );
188 }
while ( $resultset->getTotalHits() > count( $contents ) );
194 if ( $terms !== [] ) {
195 $idQuery =
new Query\Terms(
'_id', $terms );
197 $query =
new Query( $idQuery );
198 $query->setSize( 25 );
199 $query->setParam(
'_source', [
'wiki',
'uri',
'content',
'localid' ] );
200 $resultset = $this->getIndex()->search( $query );
202 foreach ( $resultset->getResults() as $result ) {
203 $data = $result->getData();
206 $sourceId = preg_replace(
'~/[^/]+$~',
'', $result->getId() );
209 'source' => $contents[$sourceId],
210 'target' => $data[
'content'],
211 'context' => $data[
'localid'],
212 'quality' => $scores[$sourceId],
213 'wiki' => $data[
'wiki'],
214 'location' => $data[
'localid'] .
'/' . $targetLanguage,
215 'uri' => $data[
'uri'],
220 uasort( $suggestions,
static function ( $a, $b ) {
221 if ( $a[
'quality'] === $b[
'quality'] ) {
225 return ( $a[
'quality'] < $b[
'quality'] ) ? 1 : -1;
229 $connection->setTimeout( $oldTimeout );
237 if ( !$handle->isValid() || $handle->getCode() ===
'' ) {
251 $sourceLanguage = $handle->getGroup()->getSourceLanguage();
254 if ( $handle->getCode() !== $sourceLanguage ) {
255 $localid = $handle->getTitleForBase()->getPrefixedText();
256 $this->deleteByQuery( $this->getIndex(), Query::create(
258 ->addFilter(
new Term( [
'wiki' => WikiMap::getCurrentWikiId() ] ) )
259 ->addFilter(
new Term( [
'language' => $handle->getCode() ] ) )
260 ->addFilter(
new Term( [
'localid' => $localid ] ) ) ) );
264 if ( $targetText ===
null ) {
269 if ( $sourceLanguage ===
null ) {
273 $revId = $handle->getTitleForLanguage( $sourceLanguage )->getLatestRevID();
274 $doc = $this->createDocument( $handle, $targetText, $revId );
277 MWElasticUtils::withRetry( self::BULK_INDEX_RETRY_ATTEMPTS,
278 function () use ( $doc ) {
279 $this->getIndex()->addDocuments( [ $doc ] );
281 static function ( $e, $errors ) use ( $fname ) {
282 $c = get_class( $e );
283 $msg = $e->getMessage();
284 error_log( $fname .
": update failed ($c: $msg); retrying." );
299 $language = $handle->
getCode();
302 $wiki = WikiMap::getCurrentWikiId();
303 $globalid =
"$wiki-$localid-$revId/$language";
307 'uri' => $handle->
getTitle()->getCanonicalURL(),
308 'localid' => $localid,
309 'language' => $language,
314 return new Document( $globalid, $data,
'_doc' );
325 'number_of_shards' => $this->getShardCount(),
329 'type' =>
'edge_ngram',
337 'tokenizer' =>
'standard',
338 'filter' => [
'lowercase',
'prefix_filter' ]
341 'tokenizer' =>
'standard'
348 $replicas = $this->getReplicaCount();
349 $key = str_contains( $replicas,
'-' ) ?
'auto_expand_replicas' :
'number_of_replicas';
350 $indexSettings[
'settings'][
'index'][$key] = $replicas;
352 $this->getIndex()->create( $indexSettings, $rebuild );
361 $this->checkElasticsearchVersion();
362 $index = $this->getIndex();
363 if ( $this->updateMapping ) {
364 $this->logOutput(
'Updating the index mappings...' );
365 $this->createIndex(
true );
366 } elseif ( !$index->exists() ) {
367 $this->createIndex(
false );
370 $settings = $index->getSettings();
371 $settings->setRefreshInterval(
'-1' );
373 $this->deleteByQuery( $this->getIndex(), Query::create(
374 (
new Term() )->setTerm(
'wiki', WikiMap::getCurrentWikiId() ) ) );
377 'wiki' => [
'type' =>
'keyword' ],
378 'localid' => [
'type' =>
'keyword' ],
379 'uri' => [
'type' =>
'keyword' ],
380 'language' => [
'type' =>
'keyword' ],
381 'group' => [
'type' =>
'keyword' ],
387 'term_vector' =>
'yes'
389 'prefix_complete' => [
391 'analyzer' =>
'prefix',
392 'search_analyzer' =>
'standard',
393 'term_vector' =>
'yes'
395 'case_sensitive' => [
397 'analyzer' =>
'casesensitive',
398 'term_vector' =>
'yes'
403 if ( $this->useElastica6() ) {
406 $mapping = new \Elastica\Type\Mapping();
408 $mapping->setType( $index->getType(
'_doc' ) );
410 $mapping->setProperties( $properties );
412 $mapping->send( [
'include_type_name' =>
'true' ] );
415 $mapping = new \Elastica\Mapping( $properties );
416 $mapping->send( $index, [
'include_type_name' =>
'false' ] );
419 $this->waitUntilReady();
429 public function batchInsertDefinitions( array $batch ): void {
430 $lb = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch();
431 foreach ( $batch as $data ) {
432 $lb->addObj( $data[0]->getTitle() );
436 $this->batchInsertTranslations( $batch );
441 foreach ( $batch as $data ) {
442 [ $handle, $sourceLanguage, $text ] = $data;
443 $revId = $handle->getTitleForLanguage( $sourceLanguage )->getLatestRevID();
444 $docs[] = $this->createDocument( $handle, $text, $revId );
447 MWElasticUtils::withRetry( self::BULK_INDEX_RETRY_ATTEMPTS,
448 function () use ( $docs ) {
449 $this->getIndex()->addDocuments( $docs );
451 function ( $e, $errors ) {
452 $c = get_class( $e );
453 $msg = $e->getMessage();
454 $this->logOutput(
"Batch failed ($c: $msg), trying again in 10 seconds" );
463 public function endBootstrap(): void {
464 $index = $this->getIndex();
466 $index->forcemerge();
467 $index->getSettings()->setRefreshInterval(
'5s' );
470 public function getClient() {
471 if ( !$this->client ) {
472 if ( isset( $this->config[
'config'] ) ) {
473 $this->client =
new Client( $this->config[
'config'] );
475 $this->client =
new Client();
478 return $this->client;
483 return isset( $this->config[
'use_wikimedia_extra'] ) && $this->config[
'use_wikimedia_extra'];
487 private function getIndexName() {
488 return $this->config[
'index'] ??
'ttmserver';
491 public function getIndex() {
492 return $this->getClient()
493 ->getIndex( $this->getIndexName() );
496 protected function getShardCount() {
497 return $this->config[
'shards'] ?? 1;
500 protected function getReplicaCount() {
501 return $this->config[
'replicas'] ??
'0-2';
504 protected function waitUntilReady() {
505 $statuses = MWElasticUtils::waitForGreen(
507 $this->getIndexName(),
508 self::WAIT_UNTIL_READY_TIMEOUT );
509 $this->logOutput(
"Waiting for the index to go green..." );
510 foreach ( $statuses as $message ) {
511 $this->logOutput( $message );
514 if ( !$statuses->getReturn() ) {
515 die(
"Timeout! Please check server logs for {$this->getIndexName()}." );
519 public function setLogger( $logger ) {
520 $this->logger = $logger;
524 protected function logOutput( $text ) {
525 if ( $this->logger ) {
526 $this->logger->statusLine(
"$text\n" );
531 $this->updateMapping = true;
541 $fields = $highlights = [];
542 $terms = preg_split(
'/\s+/', $queryString );
543 $match = $opts[
'match'];
544 $case = $opts[
'case'];
547 foreach ( $terms as $term ) {
548 $prefix = strstr( $term,
'*',
true );
551 $fields[
'content.prefix_complete'][] = $prefix;
552 } elseif ( $case ===
'1' ) {
554 $fields[
'content.case_sensitive'][] = $term;
556 $fields[
'content'][] = $term;
562 $searchQuery =
new BoolQuery();
563 foreach ( $fields as $analyzer => $words ) {
564 foreach ( $words as $word ) {
565 $boolQuery =
new BoolQuery();
566 $contentQuery =
new MatchQuery();
567 $contentQuery->setFieldQuery( $analyzer, $word );
568 $boolQuery->addShould( $contentQuery );
569 $messageQuery =
new Term();
570 $messageQuery->setTerm(
'localid', $word );
571 $boolQuery->addShould( $messageQuery );
573 if ( $match ===
'all' ) {
574 $searchQuery->addMust( $boolQuery );
576 $searchQuery->addShould( $boolQuery );
580 $highlights[$analyzer] = [
581 'number_of_fragments' => 0
586 $title = Title::newFromText( $word );
591 if ( $handle->isValid() && $handle->getCode() !==
'' ) {
592 $localid = $handle->getTitleForBase()->getPrefixedText();
593 $boolQuery =
new BoolQuery();
594 $messageId =
new Term();
595 $messageId->setTerm(
'localid', $localid );
596 $boolQuery->addMust( $messageId );
597 $searchQuery->addShould( $boolQuery );
602 return [ $searchQuery, $highlights ];
613 $query =
new Query();
615 [ $searchQuery, $highlights ] = $this->parseQueryString( $queryString, $opts );
616 $query->setQuery( $searchQuery );
618 $language =
new Terms(
'language' );
619 $language->setField(
'language' );
620 $language->setSize( 500 );
621 $query->addAggregation( $language );
623 $group =
new Terms(
'group' );
624 $group->setField(
'group' );
627 $group->setSize( 500 );
628 $query->addAggregation( $group );
630 $query->setSize( $opts[
'limit'] );
631 $query->setFrom( $opts[
'offset'] );
637 $filters =
new BoolQuery();
639 $language = $opts[
'language'];
640 if ( $language !==
'' ) {
641 $languageFilter =
new Term();
642 $languageFilter->setTerm(
'language', $language );
643 $filters->addFilter( $languageFilter );
646 $group = $opts[
'group'];
647 if ( $group !==
'' ) {
648 $groupFilter =
new Term();
649 $groupFilter->setTerm(
'group', $group );
650 $filters->addFilter( $groupFilter );
654 if ( $language !==
'' || $group !==
'' ) {
658 $query->setPostFilter( $filters );
661 [ $pre, $post ] = $highlight;
662 $query->setHighlight( [
664 'pre_tags' => [ $pre ],
665 'post_tags' => [ $post ],
666 'fields' => $highlights,
669 return $this->getIndex()->createSearch( $query );
680 public function search( $queryString, $opts, $highlight ) {
681 $search = $this->createSearch( $queryString, $opts, $highlight );
684 return $search->search();
685 }
catch ( ExceptionInterface $e ) {
692 $this->assertResultSetInstance( $resultset );
693 $aggs = $resultset->getAggregations();
694 '@phan-var array[][][] $aggs';
701 foreach ( $aggs as $type => $info ) {
702 foreach ( $info[
'buckets'] as $row ) {
703 $ret[$type][$row[
'key']] = $row[
'doc_count'];
712 $this->assertResultSetInstance( $resultset );
713 return $resultset->getTotalHits();
718 $this->assertResultSetInstance( $resultset );
720 foreach ( $resultset->getResults() as $document ) {
721 $data = $document->getData();
722 $hl = $document->getHighlights();
723 if ( isset( $hl[
'content.prefix_complete'][0] ) ) {
724 $data[
'content'] = $hl[
'content.prefix_complete'][0];
725 } elseif ( isset( $hl[
'content.case_sensitive'][0] ) ) {
726 $data[
'content'] = $hl[
'content.case_sensitive'][0];
727 } elseif ( isset( $hl[
'content'][0] ) ) {
728 $data[
'content'] = $hl[
'content'][0];
745 private function deleteByQuery( \Elastica\Index $index, Query $query ) {
747 MWElasticUtils::deleteByQuery( $index, $query,
true );
748 }
catch ( Exception $e ) {
749 LoggerFactory::getInstance(
'ElasticSearchTTMServer' )->error(
750 'Problem encountered during deletion.',
751 [
'exception' => $e ]
754 throw new RuntimeException(
"Problem encountered during deletion.\n" . $e );
759 private function getElasticsearchVersion(): string {
760 $response = $this->getClient()->request(
'' );
761 if ( !$response->isOK() ) {
762 throw new \RuntimeException(
"Cannot fetch elasticsearch version: " . $response->getError() );
765 $result = $response->getData();
766 if ( !isset( $result[
'version'][
'number'] ) ) {
767 throw new \RuntimeException(
'Unable to determine elasticsearch version, aborting.' );
770 return $result[
'version' ][
'number' ];
773 private function checkElasticsearchVersion() {
774 $version = $this->getElasticsearchVersion();
775 if ( !str_starts_with( $version,
'6.8' ) && !str_starts_with( $version,
'7.' ) ) {
776 throw new \RuntimeException(
"Only Elasticsearch 6.8.x and 7.x are supported. Your version: $version." );
780 private function useElastica6(): bool {
781 return class_exists(
'\Elastica\Type' );
784 private function assertResultSetInstance( $resultset ): void {
785 if ( $resultset instanceof ResultSet ) {
789 throw new RuntimeException(
790 "Expected resultset to be an instance of " . ResultSet::class
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