Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MessageCollection.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageLoading;
5
6use AppendIterator;
7use ArrayAccess;
8use Countable;
9use EmptyIterator;
10use InvalidArgumentException;
11use Iterator;
12use LogicException;
13use MediaWiki\Content\TextContent;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Revision\SlotRecord;
19use MediaWiki\Title\TitleValue;
20use RuntimeException;
21use stdClass;
22use Traversable;
23use Wikimedia\Rdbms\IDatabase;
24use Wikimedia\Rdbms\IDBAccessObject;
25
38class MessageCollection implements ArrayAccess, Iterator, Countable {
45 private const MAX_ITEMS_PER_QUERY = 2000;
46 public const FILTER_FUZZY = 'fuzzy';
47 public const FILTER_OPTIONAL = 'optional';
48 public const FILTER_IGNORED = 'ignored';
49 public const FILTER_HAS_TRANSLATION = 'hastranslation';
50 public const FILTER_CHANGED = 'changed';
51 public const FILTER_TRANSLATED = 'translated';
52 public const FILTER_REVIEWER = 'reviewer';
53 public const FILTER_LAST_TRANSLATOR = 'last-translator';
54 private const AVAILABLE_FILTERS = [
55 self::FILTER_FUZZY,
56 self::FILTER_OPTIONAL,
57 self::FILTER_IGNORED,
58 self::FILTER_HAS_TRANSLATION,
59 self::FILTER_CHANGED,
60 self::FILTER_TRANSLATED,
61 self::FILTER_REVIEWER,
62 self::FILTER_LAST_TRANSLATOR,
63 ];
64 public const INCLUDE_MATCHING = false;
65 public const EXCLUDE_MATCHING = true;
66
68 public string $code;
69 private MessageDefinitions $definitions;
71 private array $infile = [];
72 // Keys and messages.
73
75 protected array $keys = [];
77 protected ?array $messages = [];
78 private ?array $reverseMap = null;
79 // Database resources
80
82 private Traversable $dbInfo;
84 private Traversable $dbData;
86 private Traversable $dbReviewData;
92 protected array $tags = [];
94 private array $authors = [];
95
100 public function __construct( string $code ) {
101 $this->code = $code;
102 }
103
109 public static function newFromDefinitions( MessageDefinitions $definitions, string $code ): self {
110 $collection = new self( $code );
111 $collection->definitions = $definitions;
112 $collection->resetForNewLanguage( $code );
113
114 return $collection;
115 }
116
117 public function getLanguage(): string {
118 return $this->code;
119 }
120
121 // Data setters
122
128 public function setInFile( array $messages ): void {
129 $this->infile = $messages;
130 }
131
137 public function setTags( string $type, array $keys ): void {
138 $this->tags[$type] = $keys;
139 }
140
145 public function keys(): array {
146 return $this->keys;
147 }
148
153 private function getTitles(): array {
154 return array_values( $this->keys );
155 }
156
161 public function getMessageKeys(): array {
162 return array_keys( $this->keys );
163 }
164
170 public function getTags( string $type ): array {
171 return $this->tags[$type] ?? [];
172 }
173
180 public function getAuthors(): array {
181 $this->loadTranslations();
182
183 $authors = array_flip( $this->authors );
184
185 foreach ( $this->messages as $m ) {
186 // Check if there are authors
188 $author = $m->getProperty( 'last-translator-text' );
189
190 if ( $author === null ) {
191 continue;
192 }
193
194 if ( !isset( $authors[$author] ) ) {
195 $authors[$author] = 1;
196 } else {
197 $authors[$author]++;
198 }
199 }
200
201 # arsort( $authors, SORT_NUMERIC );
202 ksort( $authors );
203 $fuzzyBot = FuzzyBot::getName();
204 $filteredAuthors = [];
205 foreach ( $authors as $author => $edits ) {
206 if ( $author !== $fuzzyBot ) {
207 $filteredAuthors[] = (string)$author;
208 }
209 }
210
211 return $filteredAuthors;
212 }
213
219 public function addCollectionAuthors( array $authors, string $mode = 'append' ): void {
220 switch ( $mode ) {
221 case 'append':
222 $authors = array_merge( $this->authors, $authors );
223 break;
224 case 'set':
225 break;
226 default:
227 throw new InvalidArgumentException( "Invalid mode $mode" );
228 }
229
230 $this->authors = array_unique( $authors );
231 }
232
233 // Data modifiers
234
239 public function loadTranslations(): void {
240 // Performance optimization: Instead of building conditions based on key in every
241 // method, build them once and pass it on to each of them.
242 $dbr = Utilities::getSafeReadDB();
243 $titleConds = $this->getTitleConds( $dbr );
244
245 $this->loadData( $this->keys, $titleConds );
246 $this->loadInfo( $this->keys, $titleConds );
247 $this->loadReviewInfo( $this->keys, $titleConds );
248 $this->initMessages();
249 }
250
255 public function resetForNewLanguage( string $code ): void {
256 $this->code = $code;
257 $this->keys = $this->fixKeys();
258 $this->dbInfo = new EmptyIterator();
259 $this->dbData = new EmptyIterator();
260 $this->dbReviewData = new EmptyIterator();
261 $this->messages = null;
262 $this->infile = [];
263 $this->authors = [];
264
265 unset( $this->tags['fuzzy'] );
266 $this->reverseMap = null;
267 }
268
276 public function slice( $offset, $limit ) {
277 $indexes = array_keys( $this->keys );
278
279 if ( $offset === '' ) {
280 $offset = 0;
281 }
282
283 // Handle string offsets
284 if ( !ctype_digit( (string)$offset ) ) {
285 $pos = array_search( $offset, array_keys( $this->keys ), true );
286 // Now offset is always an integer, suitable for array_slice
287 $offset = $pos !== false ? $pos : count( $this->keys );
288 } else {
289 $offset = (int)$offset;
290 }
291
292 // False means that cannot go back or forward
293 $backwardsOffset = $forwardsOffset = false;
294 // Backwards paging uses numerical indexes, see below
295
296 // Can only skip this if no offset has been provided or the
297 // offset is zero. (offset - limit ) > 1 does not work, because
298 // users can end in offest=2, limit=5 and can't see the first
299 // two messages. That's also why it is capped into zero with
300 // max(). And finally make the offsets to be strings even if
301 // they are numbers in this case.
302 if ( $offset > 0 ) {
303 $backwardsOffset = (string)( max( 0, $offset - $limit ) );
304 }
305
306 // Forwards paging uses keys. If user opens view Untranslated,
307 // translates some messages and then clicks next, the first
308 // message visible in the page is the first message not shown
309 // in the previous page (unless someone else translated it at
310 // the same time). If we used integer offsets, we would skip
311 // same number of messages that were translated, because they
312 // are no longer in the list. For backwards paging this is not
313 // such a big issue, so it still uses integer offsets, because
314 // we would need to also implement "direction" to have it work
315 // correctly.
316 if ( isset( $indexes[$offset + $limit] ) ) {
317 $forwardsOffset = $indexes[$offset + $limit];
318 }
319
320 $this->keys = array_slice( $this->keys, $offset, $limit, true );
321
322 return [ $backwardsOffset, $forwardsOffset, $offset ];
323 }
324
351 public function filter( string $filter, bool $condition, ?int $value = null ): void {
352 if ( !in_array( $filter, self::AVAILABLE_FILTERS, true ) ) {
353 throw new InvalidFilterException( $filter );
354 }
355
356 $keys = $this->keys;
357 if ( $filter === self::FILTER_FUZZY ) {
358 $keys = $this->filterFuzzy( $keys, $condition );
359 } elseif ( $filter === self::FILTER_HAS_TRANSLATION ) {
360 $keys = $this->filterHastranslation( $keys, $condition );
361 } elseif ( $filter === self::FILTER_TRANSLATED ) {
362 $fuzzy = $this->filterFuzzy( $keys, self::INCLUDE_MATCHING );
363 $hastranslation = $this->filterHastranslation( $keys, self::INCLUDE_MATCHING );
364 // Fuzzy messages are not counted as translated messages
365 $translated = $this->filterOnCondition( $hastranslation, $fuzzy );
366 $keys = $this->filterOnCondition( $keys, $translated, $condition );
367 } elseif ( $filter === self::FILTER_CHANGED ) {
368 $keys = $this->filterChanged( $keys, $condition );
369 } elseif ( $filter === self::FILTER_REVIEWER ) {
370 $keys = $this->filterReviewer( $keys, $condition, $value );
371 } elseif ( $filter === self::FILTER_LAST_TRANSLATOR ) {
372 $keys = $this->filterLastTranslator( $keys, $condition, $value );
373 } else {
374 if ( !isset( $this->tags[$filter] ) ) {
375 if ( $filter !== self::FILTER_OPTIONAL && $filter !== self::FILTER_IGNORED ) {
376 throw new RuntimeException( "No tagged messages for custom filter $filter" );
377 }
378 $keys = $this->filterOnCondition( $keys, [], $condition );
379 } else {
380 $taggedKeys = array_flip( $this->tags[$filter] );
381 $keys = $this->filterOnCondition( $keys, $taggedKeys, $condition );
382 }
383 }
384
385 $this->keys = $keys;
386 }
387
389 public function filterUntranslatedOptional(): void {
390 $optionalKeys = array_flip( $this->tags['optional'] ?? [] );
391 // Convert plain message keys to array<string,TitleValue>
392 $optional = $this->filterOnCondition( $this->keys, $optionalKeys, self::INCLUDE_MATCHING );
393 // Then get reduce that list to those which have no translation. Ensure we don't
394 // accidentally populate the info cache with too few keys.
395 $this->loadInfo( $this->keys );
396 $untranslatedOptional = $this->filterHastranslation( $optional, self::EXCLUDE_MATCHING );
397 // Now remove that list from the full list
398 $this->keys = $this->filterOnCondition( $this->keys, $untranslatedOptional );
399 }
400
416 private function filterOnCondition( array $keys, array $condKeys, bool $condition = true ): array {
417 if ( $condition === self::EXCLUDE_MATCHING ) {
418 // Delete $condKeys from $keys
419 foreach ( array_keys( $condKeys ) as $key ) {
420 unset( $keys[$key] );
421 }
422 } else {
423 // Keep the keys which are in $condKeys
424 foreach ( array_keys( $keys ) as $key ) {
425 if ( !isset( $condKeys[$key] ) ) {
426 unset( $keys[$key] );
427 }
428 }
429 }
430
431 return $keys;
432 }
433
441 private function filterFuzzy( array $keys, bool $condition ): array {
442 $this->loadInfo( $keys );
443
444 $origKeys = [];
445 if ( $condition === self::INCLUDE_MATCHING ) {
446 $origKeys = $keys;
447 }
448
449 foreach ( $this->dbInfo as $row ) {
450 if ( $row->rt_type !== null ) {
451 unset( $keys[$this->rowToKey( $row )] );
452 }
453 }
454
455 if ( $condition === self::INCLUDE_MATCHING ) {
456 $keys = array_diff( $origKeys, $keys );
457 }
458
459 return $keys;
460 }
461
469 private function filterHastranslation( array $keys, bool $condition ): array {
470 $this->loadInfo( $keys );
471
472 $origKeys = [];
473 if ( $condition === self::INCLUDE_MATCHING ) {
474 $origKeys = $keys;
475 }
476
477 foreach ( $this->dbInfo as $row ) {
478 unset( $keys[$this->rowToKey( $row )] );
479 }
480
481 // Check also if there is something in the file that is not yet in the database
482 foreach ( array_keys( $this->infile ) as $inf ) {
483 unset( $keys[$inf] );
484 }
485
486 // Remove the messages which do not have a translation from the list
487 if ( $condition === self::INCLUDE_MATCHING ) {
488 $keys = array_diff( $origKeys, $keys );
489 }
490
491 return $keys;
492 }
493
502 private function filterChanged( array $keys, bool $condition ): array {
503 $this->loadData( $keys );
504
505 $origKeys = [];
506 if ( $condition === self::INCLUDE_MATCHING ) {
507 $origKeys = $keys;
508 }
509
510 $revStore = MediaWikiServices::getInstance()->getRevisionStore();
511 $infileRows = [];
512 foreach ( $this->dbData as $row ) {
513 $mkey = $this->rowToKey( $row );
514 if ( isset( $this->infile[$mkey] ) ) {
515 $infileRows[] = $row;
516 }
517 }
518
519 $revisions = $revStore->newRevisionsFromBatch( $infileRows, [
520 'slots' => [ SlotRecord::MAIN ],
521 'content' => true
522 ] )->getValue();
523 foreach ( $infileRows as $row ) {
524 $content = $revisions[$row->rev_id]?->getContent( SlotRecord::MAIN );
525 if ( $content instanceof TextContent ) {
526 $mkey = $this->rowToKey( $row );
527 if ( $this->infile[$mkey] === $content->getText() ) {
528 // Remove unchanged messages from the list
529 unset( $keys[$mkey] );
530 }
531 }
532 }
533
534 // Remove the messages which have changed from the original list
535 if ( $condition === self::INCLUDE_MATCHING ) {
536 $keys = $this->filterOnCondition( $origKeys, $keys );
537 }
538
539 return $keys;
540 }
541
550 private function filterReviewer( array $keys, bool $condition, ?int $userId ): array {
551 $this->loadReviewInfo( $keys );
552 $origKeys = $keys;
553
554 /* This removes messages from the list which have certain
555 * reviewer (among others) */
556 foreach ( $this->dbReviewData as $row ) {
557 if ( $userId === null || (int)$row->trr_user === $userId ) {
558 unset( $keys[$this->rowToKey( $row )] );
559 }
560 }
561
562 if ( $condition === self::INCLUDE_MATCHING ) {
563 $keys = array_diff( $origKeys, $keys );
564 }
565
566 return $keys;
567 }
568
575 private function filterLastTranslator( array $keys, bool $condition, ?int $userId ): array {
576 $this->loadData( $keys );
577 $origKeys = $keys;
578
579 $userId ??= 0;
580 foreach ( $this->dbData as $row ) {
581 if ( (int)$row->rev_user === $userId ) {
582 unset( $keys[$this->rowToKey( $row )] );
583 }
584 }
585
586 if ( $condition === self::INCLUDE_MATCHING ) {
587 $keys = array_diff( $origKeys, $keys );
588 }
589
590 return $keys;
591 }
592
597 private function fixKeys(): array {
598 $newkeys = [];
599
600 $pages = $this->definitions->getPages();
601 foreach ( $pages as $key => $baseTitle ) {
602 $newkeys[$key] = new TitleValue(
603 $baseTitle->getNamespace(),
604 $baseTitle->getDBkey() . '/' . $this->code
605 );
606 }
607
608 return $newkeys;
609 }
610
616 private function loadInfo( array $keys, ?array $titleConds = null ): void {
617 if ( !$this->dbInfo instanceof EmptyIterator ) {
618 return;
619 }
620
621 if ( !count( $keys ) ) {
622 $this->dbInfo = new EmptyIterator();
623 return;
624 }
625
626 $dbr = Utilities::getSafeReadDB();
627
628 $titleConds ??= $this->getTitleConds( $dbr );
629 $iterator = new AppendIterator();
630 foreach ( $titleConds as $conds ) {
631 $queryResults = $dbr->newSelectQueryBuilder()
632 ->select( [ 'page_namespace', 'page_title', 'rt_type' ] )
633 ->from( 'page' )
634 ->leftJoin( 'revtag', null, [
635 'page_id=rt_page',
636 'page_latest=rt_revision',
637 'rt_type' => RevTagStore::FUZZY_TAG,
638 ] )
639 ->where( $conds )
640 ->caller( __METHOD__ )
641 ->fetchResultSet();
642 $iterator->append( $queryResults );
643 }
644
645 $this->dbInfo = $iterator;
646
647 // Populate and cache reverse map now, since if call to initMesages is delayed (e.g. a
648 // filter that calls loadData() is used, or ::slice is used) the reverse map will not
649 // contain all the entries that are present in our $iterator and will throw notices.
650 $this->getReverseMap();
651 }
652
658 private function loadReviewInfo( array $keys, ?array $titleConds = null ): void {
659 if ( !$this->dbReviewData instanceof EmptyIterator ) {
660 return;
661 }
662
663 if ( !count( $keys ) ) {
664 $this->dbReviewData = new EmptyIterator();
665 return;
666 }
667
668 $dbr = Utilities::getSafeReadDB();
669
670 $titleConds ??= $this->getTitleConds( $dbr );
671 $iterator = new AppendIterator();
672 foreach ( $titleConds as $conds ) {
673 $queryResults = $dbr->newSelectQueryBuilder()
674 ->select( [ 'page_namespace', 'page_title', 'trr_user' ] )
675 ->from( 'page' )
676 ->join( 'translate_reviews', null, [ 'page_id=trr_page', 'page_latest=trr_revision' ] )
677 ->where( $conds )
678 ->caller( __METHOD__ )
679 ->fetchResultSet();
680 $iterator->append( $queryResults );
681 }
682
683 $this->dbReviewData = $iterator;
684
685 // Populate and cache reverse map now, since if call to initMesages is delayed (e.g. a
686 // filter that calls loadData() is used, or ::slice is used) the reverse map will not
687 // contain all the entries that are present in our $iterator and will throw notices.
688 $this->getReverseMap();
689 }
690
696 private function loadData( array $keys, ?array $titleConds = null ): void {
697 if ( !$this->dbData instanceof EmptyIterator ) {
698 return;
699 }
700
701 if ( !count( $keys ) ) {
702 $this->dbData = new EmptyIterator();
703 return;
704 }
705
706 $dbr = Utilities::getSafeReadDB();
707 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
708
709 $titleConds ??= $this->getTitleConds( $dbr );
710 $iterator = new AppendIterator();
711 foreach ( $titleConds as $conds ) {
712 $queryResults = $revisionStore->newSelectQueryBuilder( $dbr )
713 ->joinPage()
714 ->joinComment()
715 ->where( $conds )
716 ->andWhere( [ 'page_latest = rev_id' ] )
717 ->caller( __METHOD__ )
718 ->fetchResultSet();
719 $iterator->append( $queryResults );
720 }
721
722 $this->dbData = $iterator;
723
724 // Populate and cache reverse map now, since if call to initMesages is delayed (e.g. a
725 // filter that calls loadData() is used, or ::slice is used) the reverse map will not
726 // contain all the entries that are present in our $iterator and will throw notices.
727 $this->getReverseMap();
728 }
729
734 private function getTitleConds( IDatabase $db ): array {
735 $titles = $this->getTitles();
736 $chunks = array_chunk( $titles, self::MAX_ITEMS_PER_QUERY );
737 $results = [];
738
739 foreach ( $chunks as $titles ) {
740 // Array of array( namespace, pagename )
741 $byNamespace = [];
742 foreach ( $titles as $title ) {
743 $namespace = $title->getNamespace();
744 $pagename = $title->getDBkey();
745 $byNamespace[$namespace][] = $pagename;
746 }
747
748 $conds = [];
749 foreach ( $byNamespace as $namespaces => $pagenames ) {
750 $cond = [
751 'page_namespace' => $namespaces,
752 'page_title' => $pagenames,
753 ];
754
755 $conds[] = $db->makeList( $cond, LIST_AND );
756 }
757
758 $results[] = $db->makeList( $conds, LIST_OR );
759 }
760
761 return $results;
762 }
763
769 private function rowToKey( stdClass $row ): ?string {
770 $map = $this->getReverseMap();
771 if ( isset( $map[$row->page_namespace][$row->page_title] ) ) {
772 return $map[$row->page_namespace][$row->page_title];
773 } else {
774 wfWarn( "Got unknown title from the database: {$row->page_namespace}:{$row->page_title}" );
775
776 return null;
777 }
778 }
779
781 private function getReverseMap(): array {
782 if ( $this->reverseMap !== null ) {
783 return $this->reverseMap;
784 }
785
786 $map = [];
788 foreach ( $this->keys as $mkey => $title ) {
789 $map[$title->getNamespace()][$title->getDBkey()] = $mkey;
790 }
791
792 $this->reverseMap = $map;
793 return $this->reverseMap;
794 }
795
800 public function initMessages(): void {
801 if ( $this->messages !== null ) {
802 return;
803 }
804
805 $messages = [];
806 $definitions = $this->definitions->getDefinitions();
807 $revStore = MediaWikiServices::getInstance()->getRevisionStore();
808 $queryFlags = Utilities::shouldReadFromPrimary() ? IDBAccessObject::READ_LATEST : 0;
809 foreach ( array_keys( $this->keys ) as $mkey ) {
810 $messages[$mkey] = new ThinMessage( $mkey, $definitions[$mkey] );
811 }
812
813 if ( !$this->dbData instanceof EmptyIterator ) {
814 $slotRows = $revStore->getContentBlobsForBatch(
815 $this->dbData,
816 [ SlotRecord::MAIN ],
817 $queryFlags
818 )->getValue();
819
820 foreach ( $this->dbData as $row ) {
821 $mkey = $this->rowToKey( $row );
822 if ( !isset( $messages[$mkey] ) ) {
823 continue;
824 }
825 $messages[$mkey]->setRow( $row );
826 $messages[$mkey]->setProperty( 'revision', $row->page_latest );
827
828 if ( isset( $slotRows[$row->rev_id][SlotRecord::MAIN] ) ) {
829 $slot = $slotRows[$row->rev_id][SlotRecord::MAIN];
830 $messages[$mkey]->setTranslation( $slot->blob_data );
831 }
832 }
833 }
834
835 $fuzzy = [];
836 foreach ( $this->dbInfo as $row ) {
837 if ( $row->rt_type !== null ) {
838 $fuzzy[] = $this->rowToKey( $row );
839 }
840 }
841
842 $this->setTags( 'fuzzy', $fuzzy );
843
844 // Copy tags if any.
845 foreach ( $this->tags as $type => $keys ) {
846 foreach ( $keys as $mkey ) {
847 if ( isset( $messages[$mkey] ) ) {
848 $messages[$mkey]->addTag( $type );
849 }
850 }
851 }
852
853 // Copy infile if any.
854 foreach ( $this->infile as $mkey => $value ) {
855 if ( isset( $messages[$mkey] ) ) {
856 $messages[$mkey]->setInfile( $value );
857 }
858 }
859
860 foreach ( $this->dbReviewData as $row ) {
861 $mkey = $this->rowToKey( $row );
862 if ( !isset( $messages[$mkey] ) ) {
863 continue;
864 }
865 $messages[$mkey]->appendProperty( 'reviewers', $row->trr_user );
866 }
867
868 // Set the status property
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' );
876 } else {
877 $obj->setProperty( 'status', 'untranslated' );
878 }
879 }
880
881 $this->messages = $messages;
882 }
883
888 public function offsetExists( $offset ): bool {
889 return isset( $this->keys[$offset] );
890 }
891
893 public function offsetGet( $offset ): ?Message {
894 return $this->messages[$offset] ?? null;
895 }
896
901 public function offsetSet( $offset, $value ): void {
902 $this->messages[$offset] = $value;
903 }
904
906 public function offsetUnset( $offset ): void {
907 unset( $this->keys[$offset] );
908 }
909
916 public function __get( string $name ): void {
917 throw new LogicException( __METHOD__ . ": Trying to access unknown property $name" );
918 }
919
925 public function __set( string $name, $value ): void {
926 throw new LogicException( __METHOD__ . ": Trying to modify unknown property $name" );
927 }
928
934 public function rewind(): void {
935 reset( $this->keys );
936 }
937
939 #[\ReturnTypeWillChange]
940 public function current() {
941 if ( !count( $this->keys ) ) {
942 return false;
943 }
944
945 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
946 return $this->messages[key( $this->keys )];
947 }
948
949 public function key(): string {
950 return key( $this->keys );
951 }
952
953 public function next(): void {
954 next( $this->keys );
955 }
956
957 public function valid(): bool {
958 return isset( $this->messages[key( $this->keys )] );
959 }
960
961 public function count(): int {
962 return count( $this->keys() );
963 }
964
966}
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->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);}, 'Translate:WorkflowStatesMessageGroupLoader'=> static function(MediaWikiServices $services):WorkflowStatesMessageGroupLoader { return new WorkflowStatesMessageGroupLoader(new ServiceOptions(WorkflowStatesMessageGroupLoader::CONSTRUCTOR_OPTIONS, $services->getMainConfig()),);},]
@phpcs-require-sorted-array
Class to manage revision tags for translatable bundles.
This file contains the class for core message collections implementation.
array $messages
array( Message String => Message, ... )
resetForNewLanguage(string $code)
Some statistics scripts for example loop the same collection over every language.
getAuthors()
Lists all translators that have contributed to the latest revisions of each translation.
addCollectionAuthors(array $authors, string $mode='append')
Add external authors (usually from the file).
static newFromDefinitions(MessageDefinitions $definitions, string $code)
Construct a new message collection from definitions.
setInFile(array $messages)
Set translation from file, as opposed to translation which only exists in the wiki because they are n...
__get(string $name)
Fail fast if trying to access unknown properties.
filter(string $filter, bool $condition, ?int $value=null)
Filters messages based on some condition.
initMessages()
Constructs all Messages (ThinMessage) from the data accumulated so far.
getMessageKeys()
Returns list of message keys that are used in this collection after filtering.
__set(string $name, $value)
Fail fast if trying to access unknown properties.
Wrapper for message definitions, just to beauty the code.
Interface for message objects used by MessageCollection.
Definition Message.php:13
Message object which is based on database result row.
FuzzyBot - the misunderstood workhorse.
Definition FuzzyBot.php:15
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:29