45 private const MAX_ITEMS_PER_QUERY = 2000;
51 private array $infile = [];
58 private ?array $reverseMap;
62 private Traversable $dbInfo;
64 private Traversable $dbData;
66 private Traversable $dbReviewData;
72 protected array $tags = [];
74 private array $authors = [];
90 $collection = new self(
$code );
91 $collection->definitions = $definitions;
92 $collection->resetForNewLanguage(
$code );
97 public function getLanguage(): string {
109 $this->infile = $messages;
117 public function setTags(
string $type, array $keys ): void {
118 $this->tags[$type] = $keys;
125 public function keys(): array {
133 private function getTitles(): array {
134 return array_values( $this->keys );
142 return array_keys( $this->keys );
150 public function getTags(
string $type ): array {
151 return $this->tags[$type] ?? [];
161 $this->loadTranslations();
163 $authors = array_flip( $this->authors );
165 foreach ( $this->messages as $m ) {
168 $author = $m->getProperty(
'last-translator-text' );
170 if ( $author ===
null ) {
174 if ( !isset( $authors[$author] ) ) {
175 $authors[$author] = 1;
181 # arsort( $authors, SORT_NUMERIC );
183 $fuzzyBot = FuzzyBot::getName();
184 $filteredAuthors = [];
185 foreach ( $authors as $author => $edits ) {
186 if ( $author !== $fuzzyBot ) {
187 $filteredAuthors[] = $author;
191 return $filteredAuthors;
202 $authors = array_merge( $this->authors, $authors );
207 throw new InvalidArgumentException(
"Invalid mode $mode" );
210 $this->authors = array_unique( $authors );
223 $titleConds = $this->getTitleConds( $dbr );
225 $this->loadData( $this->keys, $titleConds );
226 $this->loadInfo( $this->keys, $titleConds );
227 $this->loadReviewInfo( $this->keys, $titleConds );
228 $this->initMessages();
237 $this->keys = $this->fixKeys();
238 $this->dbInfo =
new EmptyIterator();
239 $this->dbData =
new EmptyIterator();
240 $this->dbReviewData =
new EmptyIterator();
241 $this->messages =
null;
245 unset( $this->tags[
'fuzzy'] );
246 $this->reverseMap =
null;
256 public function slice( $offset, $limit ) {
257 $indexes = array_keys( $this->keys );
259 if ( $offset ===
'' ) {
264 if ( !ctype_digit( (
string)$offset ) ) {
265 $pos = array_search( $offset, array_keys( $this->keys ),
true );
267 $offset = $pos !==
false ? $pos : count( $this->keys );
269 $offset = (int)$offset;
273 $backwardsOffset = $forwardsOffset =
false;
283 $backwardsOffset = (string)( max( 0, $offset - $limit ) );
296 if ( isset( $indexes[$offset + $limit] ) ) {
297 $forwardsOffset = $indexes[$offset + $limit];
300 $this->keys = array_slice( $this->keys, $offset, $limit,
true );
302 return [ $backwardsOffset, $forwardsOffset, $offset ];
328 public function filter(
string $type,
bool $condition =
true, ?
int $value =
null ): void {
329 if ( !in_array( $type, self::getAvailableFilters(), true ) ) {
332 $this->applyFilter( $type, $condition, $value );
335 private static function getAvailableFilters(): array {
355 private function applyFilter(
string $filter,
bool $condition, ?
int $value ): void {
357 if ( $filter ===
'fuzzy' ) {
358 $keys = $this->filterFuzzy( $keys, $condition );
359 } elseif ( $filter ===
'hastranslation' ) {
360 $keys = $this->filterHastranslation( $keys, $condition );
361 } elseif ( $filter ===
'translated' ) {
362 $fuzzy = $this->filterFuzzy( $keys,
false );
363 $hastranslation = $this->filterHastranslation( $keys,
false );
365 $translated = $this->filterOnCondition( $hastranslation, $fuzzy );
366 $keys = $this->filterOnCondition( $keys, $translated, $condition );
367 } elseif ( $filter ===
'changed' ) {
368 $keys = $this->filterChanged( $keys, $condition );
369 } elseif ( $filter ===
'reviewer' ) {
370 $keys = $this->filterReviewer( $keys, $condition, $value );
371 } elseif ( $filter ===
'last-translator' ) {
372 $keys = $this->filterLastTranslator( $keys, $condition, $value );
375 if ( !isset( $this->tags[$filter] ) ) {
376 if ( $filter !==
'optional' && $filter !==
'ignored' ) {
377 throw new RuntimeException(
"No tagged messages for custom filter $filter" );
379 $keys = $this->filterOnCondition( $keys, [], $condition );
381 $taggedKeys = array_flip( $this->tags[$filter] );
382 $keys = $this->filterOnCondition( $keys, $taggedKeys, $condition );
390 public function filterUntranslatedOptional(): void {
391 $optionalKeys = array_flip( $this->tags[
'optional'] ?? [] );
393 $optional = $this->filterOnCondition( $this->keys, $optionalKeys,
false );
396 $this->loadInfo( $this->keys );
397 $untranslatedOptional = $this->filterHastranslation( $optional,
true );
399 $this->keys = $this->filterOnCondition( $this->keys, $untranslatedOptional );
417 private function filterOnCondition( array $keys, array $condKeys,
bool $condition =
true ): array {
420 foreach ( array_keys( $condKeys ) as $key ) {
421 unset( $keys[$key] );
425 foreach ( array_keys( $keys ) as $key ) {
426 if ( !isset( $condKeys[$key] ) ) {
427 unset( $keys[$key] );
442 private function filterFuzzy( array $keys,
bool $condition ): array {
443 $this->loadInfo( $keys );
450 foreach ( $this->dbInfo as $row ) {
451 if ( $row->rt_type !==
null ) {
452 unset( $keys[$this->rowToKey( $row )] );
457 $keys = array_diff( $origKeys, $keys );
470 private function filterHastranslation( array $keys,
bool $condition ): array {
471 $this->loadInfo( $keys );
478 foreach ( $this->dbInfo as $row ) {
479 unset( $keys[$this->rowToKey( $row )] );
483 foreach ( array_keys( $this->infile ) as $inf ) {
484 unset( $keys[$inf] );
489 $keys = array_diff( $origKeys, $keys );
503 private function filterChanged( array $keys,
bool $condition ): array {
504 $this->loadData( $keys );
511 $revStore = MediaWikiServices::getInstance()->getRevisionStore();
513 foreach ( $this->dbData as $row ) {
514 $mkey = $this->rowToKey( $row );
515 if ( isset( $this->infile[$mkey] ) ) {
516 $infileRows[] = $row;
520 $revisions = $revStore->newRevisionsFromBatch( $infileRows, [
521 'slots' => [ SlotRecord::MAIN ],
524 foreach ( $infileRows as $row ) {
526 $rev = $revisions[$row->rev_id];
529 $content = $rev->getContent( SlotRecord::MAIN );
531 $mkey = $this->rowToKey( $row );
532 if ( $this->infile[$mkey] === $content->getText() ) {
534 unset( $keys[$mkey] );
542 $keys = $this->filterOnCondition( $origKeys, $keys );
556 private function filterReviewer( array $keys,
bool $condition, ?
int $userId ): array {
557 $this->loadReviewInfo( $keys );
562 foreach ( $this->dbReviewData as $row ) {
563 if ( $userId ===
null || (
int)$row->trr_user === $userId ) {
564 unset( $keys[$this->rowToKey( $row )] );
569 $keys = array_diff( $origKeys, $keys );
581 private function filterLastTranslator( array $keys,
bool $condition, ?
int $userId ): array {
582 $this->loadData( $keys );
585 $userId = $userId ?? 0;
586 foreach ( $this->dbData as $row ) {
587 if ( (
int)$row->rev_user === $userId ) {
588 unset( $keys[$this->rowToKey( $row )] );
593 $keys = array_diff( $origKeys, $keys );
603 private function fixKeys(): array {
606 $pages = $this->definitions->getPages();
607 foreach ( $pages as $key => $baseTitle ) {
608 $newkeys[$key] =
new TitleValue(
609 $baseTitle->getNamespace(),
610 $baseTitle->getDBkey() .
'/' . $this->code
622 private function loadInfo( array $keys, ?array $titleConds =
null ): void {
623 if ( !$this->dbInfo instanceof EmptyIterator ) {
627 if ( !count( $keys ) ) {
628 $this->dbInfo =
new EmptyIterator();
632 $dbr = Utilities::getSafeReadDB();
633 $tables = [
'page',
'revtag' ];
634 $fields = [
'page_namespace',
'page_title',
'rt_type' ];
635 $joins = [
'revtag' =>
638 [
'page_id=rt_page',
'page_latest=rt_revision',
'rt_type' => RevTagStore::FUZZY_TAG ]
642 $titleConds = $titleConds ?? $this->getTitleConds( $dbr );
643 $iterator =
new AppendIterator();
644 foreach ( $titleConds as $conds ) {
645 $iterator->append( $dbr->select( $tables, $fields, $conds, __METHOD__, [], $joins ) );
648 $this->dbInfo = $iterator;
653 $this->getReverseMap();
661 private function loadReviewInfo( array $keys, ?array $titleConds =
null ): void {
662 if ( !$this->dbReviewData instanceof EmptyIterator ) {
666 if ( !count( $keys ) ) {
667 $this->dbReviewData =
new EmptyIterator();
671 $dbr = Utilities::getSafeReadDB();
672 $tables = [
'page',
'translate_reviews' ];
673 $fields = [
'page_namespace',
'page_title',
'trr_user' ];
674 $joins = [
'translate_reviews' =>
677 [
'page_id=trr_page',
'page_latest=trr_revision' ]
681 $titleConds = $titleConds ?? $this->getTitleConds( $dbr );
682 $iterator =
new AppendIterator();
683 foreach ( $titleConds as $conds ) {
684 $iterator->append( $dbr->select( $tables, $fields, $conds, __METHOD__, [], $joins ) );
687 $this->dbReviewData = $iterator;
692 $this->getReverseMap();
700 private function loadData( array $keys, ?array $titleConds =
null ): void {
701 if ( !$this->dbData instanceof EmptyIterator ) {
705 if ( !count( $keys ) ) {
706 $this->dbData =
new EmptyIterator();
710 $dbr = Utilities::getSafeReadDB();
711 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
712 $revQuery = $revisionStore->getQueryInfo( [
'page' ] );
713 $tables = $revQuery[
'tables'];
714 $fields = $revQuery[
'fields'];
715 $joins = $revQuery[
'joins'];
717 $titleConds = $titleConds ?? $this->getTitleConds( $dbr );
718 $iterator =
new AppendIterator();
719 foreach ( $titleConds as $conds ) {
720 $conds = [
'page_latest = rev_id', $conds ];
721 $iterator->append( $dbr->select( $tables, $fields, $conds, __METHOD__, [], $joins ) );
724 $this->dbData = $iterator;
729 $this->getReverseMap();
736 private function getTitleConds( IDatabase $db ): array {
737 $titles = $this->getTitles();
738 $chunks = array_chunk( $titles, self::MAX_ITEMS_PER_QUERY );
741 foreach ( $chunks as $titles ) {
744 foreach ( $titles as $title ) {
745 $namespace = $title->getNamespace();
746 $pagename = $title->getDBkey();
747 $byNamespace[$namespace][] = $pagename;
751 foreach ( $byNamespace as $namespaces => $pagenames ) {
753 'page_namespace' => $namespaces,
754 'page_title' => $pagenames,
757 $conds[] = $db->makeList( $cond, LIST_AND );
760 $results[] = $db->makeList( $conds, LIST_OR );
771 private function rowToKey( stdClass $row ): ?string {
772 $map = $this->getReverseMap();
773 if ( isset( $map[$row->page_namespace][$row->page_title] ) ) {
774 return $map[$row->page_namespace][$row->page_title];
776 wfWarn(
"Got unknown title from the database: {$row->page_namespace}:{$row->page_title}" );
783 private function getReverseMap(): array {
784 if ( isset( $this->reverseMap ) ) {
785 return $this->reverseMap;
790 foreach ( $this->keys as $mkey => $title ) {
791 $map[$title->getNamespace()][$title->getDBkey()] = $mkey;
794 $this->reverseMap = $map;
795 return $this->reverseMap;
803 if ( $this->messages !== null ) {
809 $revStore = MediaWikiServices::getInstance()->getRevisionStore();
810 $queryFlags = Utilities::shouldReadFromPrimary() ? $revStore::READ_LATEST : 0;
811 foreach ( array_keys( $this->keys ) as $mkey ) {
812 $messages[$mkey] =
new ThinMessage( $mkey, $definitions[$mkey] );
815 if ( !$this->dbData instanceof EmptyIterator ) {
816 $slotRows = $revStore->getContentBlobsForBatch(
817 $this->dbData, [ SlotRecord::MAIN ], $queryFlags
820 foreach ( $this->dbData as $row ) {
821 $mkey = $this->rowToKey( $row );
822 if ( !isset( $messages[$mkey] ) ) {
825 $messages[$mkey]->setRow( $row );
826 $messages[$mkey]->setProperty(
'revision', $row->page_latest );
828 if ( isset( $slotRows[$row->rev_id][SlotRecord::MAIN] ) ) {
829 $slot = $slotRows[$row->rev_id][SlotRecord::MAIN];
830 $messages[$mkey]->setTranslation( $slot->blob_data );
836 foreach ( $this->dbInfo as $row ) {
837 if ( $row->rt_type !==
null ) {
838 $fuzzy[] = $this->rowToKey( $row );
842 $this->setTags(
'fuzzy', $fuzzy );
845 foreach ( $this->tags as $type => $keys ) {
846 foreach ( $keys as $mkey ) {
847 if ( isset( $messages[$mkey] ) ) {
848 $messages[$mkey]->addTag( $type );
854 foreach ( $this->infile as $mkey => $value ) {
855 if ( isset( $messages[$mkey] ) ) {
856 $messages[$mkey]->setInfile( $value );
860 foreach ( $this->dbReviewData as $row ) {
861 $mkey = $this->rowToKey( $row );
862 if ( !isset( $messages[$mkey] ) ) {
865 $messages[$mkey]->appendProperty(
'reviewers', $row->trr_user );
869 foreach ( $messages as $obj ) {
870 if ( $obj->hasTag(
'fuzzy' ) ) {
871 $obj->setProperty(
'status',
'fuzzy' );
872 } elseif ( is_array( $obj->getProperty(
'reviewers' ) ) ) {
873 $obj->setProperty(
'status',
'proofread' );
874 } elseif ( $obj->translation() !==
null ) {
875 $obj->setProperty(
'status',
'translated' );
877 $obj->setProperty(
'status',
'untranslated' );
881 $this->messages = $messages;
889 return isset( $this->keys[$offset] );
894 return $this->messages[$offset] ?? null;
902 $this->messages[$offset] = $value;
907 unset( $this->keys[$offset] );
916 public function __get(
string $name ): void {
917 throw new LogicException( __METHOD__ .
": Trying to access unknown property $name" );
925 public function __set(
string $name, $value ): void {
926 throw new LogicException( __METHOD__ .
": Trying to modify unknown property $name" );
935 reset( $this->keys );
938 #[\ReturnTypeWillChange]
939 public function current() {
940 if ( !count( $this->keys ) ) {
945 return $this->messages[key( $this->keys )];
948 public function key(): ?string {
949 return key( $this->keys );
952 public function next(): void {
956 public function valid(): bool {
957 return isset( $this->messages[key( $this->keys )] );
960 public function count(): int {
961 return count( $this->keys() );
return[ '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:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, '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:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, '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->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, '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'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, '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());}, '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->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:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, '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'),);}, '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