13use MediaWiki\MediaWikiServices;
14use MediaWiki\Revision\RevisionRecord;
15use MediaWiki\Revision\SlotRecord;
32 private const MAX_ITEMS_PER_QUERY = 2000;
37 private $definitions =
null;
45 protected $messages = [];
55 private $dbReviewData;
66 private $properties = [];
68 private $authors = [];
85 $collection =
new self( $code );
86 $collection->definitions = $definitions;
87 $collection->resetForNewLanguage( $code );
105 $this->infile = $messages;
113 public function setTags( $type, array $keys ) {
114 $this->tags[$type] = $keys;
131 return array_values( $this->
keys );
140 return array_keys( $this->
keys );
149 return $this->tags[$type] ?? [];
161 $authors = array_flip( $this->authors );
163 foreach ( $this->messages as $m ) {
166 $author = $m->getProperty(
'last-translator-text' );
168 if ( $author ===
null ) {
172 if ( !isset( $authors[$author] ) ) {
173 $authors[$author] = 1;
179 # arsort( $authors, SORT_NUMERIC );
181 $fuzzyBot = FuzzyBot::getName();
182 $filteredAuthors = [];
183 foreach ( $authors as $author => $edits ) {
184 if ( $author !== $fuzzyBot ) {
185 $filteredAuthors[] = $author;
189 return $filteredAuthors;
201 $authors = array_merge( $this->authors, $authors );
206 throw new MWException(
"Invalid mode $mode" );
209 $this->authors = array_unique( $authors );
221 $dbr = TranslateUtils::getSafeReadDB();
240 $this->dbReviewData = [];
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 );
271 $backwardsOffset = $forwardsOffset =
false;
281 $backwardsOffset = (string)( max( 0, $offset - $limit ) );
294 if ( isset( $indexes[$offset + $limit] ) ) {
295 $forwardsOffset = $indexes[$offset + $limit];
298 $this->
keys = array_slice( $this->
keys, $offset, $limit,
true );
300 return [ $backwardsOffset, $forwardsOffset, $offset ];
326 public function filter( $type, $condition =
true, $value =
null ) {
327 if ( !in_array( $type, self::getAvailableFilters(),
true ) ) {
328 throw new MWException(
"Unknown filter $type" );
357 if ( $filter ===
'fuzzy' ) {
359 } elseif ( $filter ===
'hastranslation' ) {
361 } elseif ( $filter ===
'translated' ) {
367 } elseif ( $filter ===
'changed' ) {
369 } elseif ( $filter ===
'reviewer' ) {
371 } elseif ( $filter ===
'last-translator' ) {
375 if ( !isset( $this->tags[$filter] ) ) {
376 if ( $filter !==
'optional' && $filter !==
'ignored' ) {
377 throw new MWException(
"No tagged messages for custom filter $filter" );
381 $taggedKeys = array_flip( $this->tags[$filter] );
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 );
418 if ( $condition ===
true ) {
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] );
443 $this->loadInfo( $keys );
446 if ( $condition ===
false ) {
450 foreach ( $this->dbInfo as $row ) {
451 if ( $row->rt_type !==
null ) {
452 unset( $keys[$this->rowToKey( $row )] );
456 if ( $condition ===
false ) {
457 $keys = array_diff( $origKeys, $keys );
471 $this->loadInfo( $keys );
474 if ( $condition ===
false ) {
478 foreach ( $this->dbInfo as $row ) {
479 unset( $keys[$this->rowToKey( $row )] );
483 foreach ( array_keys( $this->infile ) as $inf ) {
484 unset( $keys[$inf] );
488 if ( $condition ===
false ) {
489 $keys = array_diff( $origKeys, $keys );
504 $this->loadData( $keys );
507 if ( $condition ===
false ) {
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] );
541 if ( $condition ===
false ) {
542 $keys = $this->filterOnCondition( $origKeys, $keys );
557 $this->loadReviewInfo( $keys );
562 $userId = (int)$user;
563 foreach ( $this->dbReviewData as $row ) {
564 if ( $user ===
null || (
int)$row->trr_user === $userId ) {
565 unset( $keys[$this->rowToKey( $row )] );
569 if ( $condition ===
false ) {
570 $keys = array_diff( $origKeys, $keys );
584 $this->loadData( $keys );
588 foreach ( $this->dbData as $row ) {
589 if ( (
int)$row->rev_user === $user ) {
590 unset( $keys[$this->rowToKey( $row )] );
594 if ( $condition ===
false ) {
595 $keys = array_diff( $origKeys, $keys );
608 $pages = $this->definitions->getPages();
609 foreach ( $pages as $key => $baseTitle ) {
610 $newkeys[$key] =
new TitleValue(
611 $baseTitle->getNamespace(),
612 $baseTitle->getDBkey() .
'/' . $this->code
624 protected function loadInfo( array $keys, ?array $titleConds =
null ) {
625 if ( $this->dbInfo !== [] ) {
629 if ( !count( $keys ) ) {
630 $this->dbInfo =
new EmptyIterator();
634 $dbr = TranslateUtils::getSafeReadDB();
635 $tables = [
'page',
'revtag' ];
636 $fields = [
'page_namespace',
'page_title',
'rt_type' ];
637 $joins = [
'revtag' =>
640 [
'page_id=rt_page',
'page_latest=rt_revision',
'rt_type' => RevTagStore::FUZZY_TAG ]
644 $titleConds = $titleConds ?? $this->getTitleConds( $dbr );
645 $iterator =
new AppendIterator();
646 foreach ( $titleConds as $conds ) {
647 $iterator->append( $dbr->select( $tables, $fields, $conds, __METHOD__, [], $joins ) );
650 $this->dbInfo = $iterator;
655 $this->getReverseMap();
664 if ( $this->dbReviewData !== [] ) {
668 if ( !count( $keys ) ) {
669 $this->dbReviewData =
new EmptyIterator();
673 $dbr = TranslateUtils::getSafeReadDB();
674 $tables = [
'page',
'translate_reviews' ];
675 $fields = [
'page_namespace',
'page_title',
'trr_user' ];
676 $joins = [
'translate_reviews' =>
679 [
'page_id=trr_page',
'page_latest=trr_revision' ]
683 $titleConds = $titleConds ?? $this->getTitleConds( $dbr );
684 $iterator =
new AppendIterator();
685 foreach ( $titleConds as $conds ) {
686 $iterator->append( $dbr->select( $tables, $fields, $conds, __METHOD__, [], $joins ) );
689 $this->dbReviewData = $iterator;
694 $this->getReverseMap();
702 protected function loadData( array $keys, ?array $titleConds =
null ) {
703 if ( $this->dbData !== [] ) {
707 if ( !count( $keys ) ) {
708 $this->dbData =
new EmptyIterator();
712 $dbr = TranslateUtils::getSafeReadDB();
713 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
714 $revQuery = $revisionStore->getQueryInfo( [
'page' ] );
715 $tables = $revQuery[
'tables'];
716 $fields = $revQuery[
'fields'];
717 $joins = $revQuery[
'joins'];
719 $titleConds = $titleConds ?? $this->getTitleConds( $dbr );
720 $iterator =
new AppendIterator();
721 foreach ( $titleConds as $conds ) {
722 $conds = [
'page_latest = rev_id', $conds ];
723 $iterator->append( $dbr->select( $tables, $fields, $conds, __METHOD__, [], $joins ) );
726 $this->dbData = $iterator;
731 $this->getReverseMap();
741 $titles = $this->getTitles();
742 $chunks = array_chunk( $titles, self::MAX_ITEMS_PER_QUERY );
745 foreach ( $chunks as $titles ) {
748 foreach ( $titles as $title ) {
749 $namespace = $title->getNamespace();
750 $pagename = $title->getDBkey();
751 $byNamespace[$namespace][] = $pagename;
755 foreach ( $byNamespace as $namespaces => $pagenames ) {
757 'page_namespace' => $namespaces,
758 'page_title' => $pagenames,
761 $conds[] = $db->makeList( $cond, LIST_AND );
764 $results[] = $db->makeList( $conds, LIST_OR );
779 $map = $this->getReverseMap();
780 if ( isset( $map[$row->page_namespace][$row->page_title] ) ) {
781 return $map[$row->page_namespace][$row->page_title];
783 wfWarn(
"Got unknown title from the database: {$row->page_namespace}:{$row->page_title}" );
795 if ( isset( $this->reverseMap ) ) {
796 return $this->reverseMap;
801 foreach ( $this->keys as $mkey => $title ) {
802 $map[$title->getNamespace()][$title->getDBkey()] = $mkey;
805 $this->reverseMap = $map;
806 return $this->reverseMap;
814 if ( $this->messages !==
null ) {
819 $definitions = $this->definitions->getDefinitions();
820 $revStore = MediaWikiServices::getInstance()->getRevisionStore();
821 $queryFlags = TranslateUtils::shouldReadFromPrimary() ? $revStore::READ_LATEST : 0;
822 foreach ( array_keys( $this->keys ) as $mkey ) {
823 $messages[$mkey] =
new ThinMessage( $mkey, $definitions[$mkey] );
826 if ( $this->dbData !==
null ) {
827 $slotRows = $revStore->getContentBlobsForBatch(
828 $this->dbData, [ SlotRecord::MAIN ], $queryFlags
831 foreach ( $this->dbData as $row ) {
832 $mkey = $this->rowToKey( $row );
833 if ( !isset( $messages[$mkey] ) ) {
836 $messages[$mkey]->setRow( $row );
837 $messages[$mkey]->setProperty(
'revision', $row->page_latest );
839 if ( isset( $slotRows[$row->rev_id][SlotRecord::MAIN] ) ) {
840 $slot = $slotRows[$row->rev_id][SlotRecord::MAIN];
841 $messages[$mkey]->setTranslation( $slot->blob_data );
846 if ( $this->dbInfo !==
null ) {
848 foreach ( $this->dbInfo as $row ) {
849 if ( $row->rt_type !==
null ) {
850 $fuzzy[] = $this->rowToKey( $row );
854 $this->setTags(
'fuzzy', $fuzzy );
858 foreach ( $this->tags as $type => $keys ) {
859 foreach ( $keys as $mkey ) {
860 if ( isset( $messages[$mkey] ) ) {
861 $messages[$mkey]->addTag( $type );
867 foreach ( $this->properties as $type => $keys ) {
868 foreach ( $keys as $mkey => $value ) {
869 if ( isset( $messages[$mkey] ) ) {
870 $messages[$mkey]->setProperty( $type, $value );
876 foreach ( $this->infile as $mkey => $value ) {
877 if ( isset( $messages[$mkey] ) ) {
878 $messages[$mkey]->setInfile( $value );
882 foreach ( $this->dbReviewData as $row ) {
883 $mkey = $this->rowToKey( $row );
884 if ( !isset( $messages[$mkey] ) ) {
887 $messages[$mkey]->appendProperty(
'reviewers', $row->trr_user );
891 foreach ( $messages as $obj ) {
892 if ( $obj->hasTag(
'fuzzy' ) ) {
893 $obj->setProperty(
'status',
'fuzzy' );
894 } elseif ( is_array( $obj->getProperty(
'reviewers' ) ) ) {
895 $obj->setProperty(
'status',
'proofread' );
896 } elseif ( $obj->translation() !==
null ) {
897 $obj->setProperty(
'status',
'translated' );
899 $obj->setProperty(
'status',
'untranslated' );
903 $this->messages = $messages;
912 return isset( $this->keys[$offset] );
920 return $this->messages[$offset] ?? null;
928 $this->messages[$offset] = $value;
933 unset( $this->keys[$offset] );
945 throw new MWException( __METHOD__ .
": Trying to access unknown property $name" );
955 public function __set( $name, $value ) {
956 throw new MWException( __METHOD__ .
": Trying to modify unknown property $name" );
965 reset( $this->keys );
968 #[\ReturnTypeWillChange]
969 public function current() {
970 if ( !count( $this->keys ) ) {
975 return $this->messages[key( $this->keys )];
978 public function key(): ?string {
979 return key( $this->keys );
982 public function next(): void {
986 public function valid(): bool {
987 return isset( $this->messages[key( $this->keys )] );
990 public function count(): int {
991 return count( $this->keys() );
1015 $this->
namespace = $namespace;
1016 $this->messages = $messages;
1021 return $this->messages;
1026 $namespace = $this->namespace;
1027 if ( $this->pages !==
null ) {
1028 return $this->pages;
1032 foreach ( array_keys( $this->messages ) as $key ) {
1033 if ( $namespace ===
false ) {
1035 [ $tns, $tkey ] = explode(
':', $key, 2 );
1036 $title = Title::makeTitleSafe( $tns, $tkey );
1038 $title = Title::makeTitleSafe( $namespace, $key );
1042 wfWarn(
"Invalid title ($namespace:)$key" );
1046 $pages[$key] = $title;
1049 $this->pages = $pages;
1051 return $this->pages;
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'), MessageIndex::singleton());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, '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: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:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, '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: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(), new RevTagStore(), $services->getDBLoadBalancer());}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(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());}, '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
Core message collection class.
setInFile(array $messages)
Set translation from file, as opposed to translation which only exists in the wiki because they are n...
__construct( $code)
Constructors.
filterReviewer(array $keys, $condition, $user)
Filters list of keys according to whether the user has accepted them.
filterHastranslation(array $keys, $condition)
Filters list of keys according to whether they have a translation.
applyFilter( $filter, $condition, $value)
Really apply a filter.
filterOnCondition(array $keys, array $condKeys, $condition=true)
Filters list of keys with other list of keys according to the condition.
filterFuzzy(array $keys, $condition)
Filters list of keys according to whether the translation is fuzzy.
getTitleConds( $db)
Of the current set of keys, construct database query conditions.
filterLastTranslator(array $keys, $condition, $user)
getAuthors()
Lists all translators that have contributed to the latest revisions of each translation.
fixKeys()
Takes list of keys and converts them into database format.
getTitles()
Returns list of TitleValues of messages that are used in this collection after filtering.
loadReviewInfo(array $keys, ?array $titleConds=null)
Loads reviewers for given messages.
getReverseMap()
Creates a two-dimensional map of namespace and pagenames.
static newFromDefinitions(MessageDefinitions $definitions, $code)
Construct a new message collection from definitions.
offsetExists( $offset)
ArrayAccess methods.
static getAvailableFilters()
initMessages()
Constructs all TMessages from the data accumulated so far.
filter( $type, $condition=true, $value=null)
Filters messages based on some condition.
resetForNewLanguage( $code)
Some statistics scripts for example loop the same collection over every language.
__get( $name)
Fail fast if trying to access unknown properties.
loadTranslations()
Loads all message data.
setTags( $type, array $keys)
Set message tags.
getTags( $type)
Returns stored message tags.
offsetSet( $offset, $value)
loadInfo(array $keys, ?array $titleConds=null)
Loads existence and fuzzy state for given list of keys.
getMessageKeys()
Returns list of message keys that are used in this collection after filtering.
filterChanged(array $keys, $condition)
Filters list of keys according to whether the current translation differs from the commited translati...
slice( $offset, $limit)
For paging messages.
addCollectionAuthors( $authors, $mode='append')
Add external authors (usually from the file).
loadData(array $keys, ?array $titleConds=null)
Loads translation for given list of keys.
__set( $name, $value)
Fail fast if trying to access unknown properties.
keys()
Returns list of available message keys.
rowToKey( $row)
Given two-dimensional map of namespace and pagenames, this uses database fields page_namespace and pa...
Wrapper for message definitions, just to beauty the code.
__construct(array $messages, $namespace=false)
Interface for message objects used by MessageCollection.
Message object which is based on database result row.