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\IDBAccessObject;
24use Wikimedia\Rdbms\IReadableDatabase;
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
187 $author = $m->getProperty( 'last-translator-text' );
188
189 if ( $author === null ) {
190 continue;
191 }
192
193 if ( !isset( $authors[$author] ) ) {
194 $authors[$author] = 1;
195 } else {
196 $authors[$author]++;
197 }
198 }
199
200 # arsort( $authors, SORT_NUMERIC );
201 ksort( $authors );
202 $fuzzyBot = FuzzyBot::getName();
203 $filteredAuthors = [];
204 foreach ( $authors as $author => $edits ) {
205 if ( $author !== $fuzzyBot ) {
206 $filteredAuthors[] = (string)$author;
207 }
208 }
209
210 return $filteredAuthors;
211 }
212
218 public function addCollectionAuthors( array $authors, string $mode = 'append' ): void {
219 switch ( $mode ) {
220 case 'append':
221 $authors = array_merge( $this->authors, $authors );
222 break;
223 case 'set':
224 break;
225 default:
226 throw new InvalidArgumentException( "Invalid mode $mode" );
227 }
228
229 $this->authors = array_unique( $authors );
230 }
231
232 // Data modifiers
233
238 public function loadTranslations(): void {
239 // Performance optimization: Instead of building conditions based on key in every
240 // method, build them once and pass it on to each of them.
241 $dbr = Utilities::getSafeReadDB();
242 $titleConds = $this->getTitleConds( $dbr );
243
244 $this->loadData( $this->keys, $titleConds );
245 $this->loadInfo( $this->keys, $titleConds );
246 $this->loadReviewInfo( $this->keys, $titleConds );
247 $this->initMessages();
248 }
249
254 public function resetForNewLanguage( string $code ): void {
255 $this->code = $code;
256 $this->keys = $this->fixKeys();
257 $this->dbInfo = new EmptyIterator();
258 $this->dbData = new EmptyIterator();
259 $this->dbReviewData = new EmptyIterator();
260 $this->messages = null;
261 $this->infile = [];
262 $this->authors = [];
263
264 unset( $this->tags['fuzzy'] );
265 $this->reverseMap = null;
266 }
267
275 public function slice( $offset, $limit ) {
276 $indexes = array_keys( $this->keys );
277
278 if ( $offset === '' ) {
279 $offset = 0;
280 }
281
282 // Handle string offsets
283 if ( !ctype_digit( (string)$offset ) ) {
284 $pos = array_search( $offset, array_keys( $this->keys ), true );
285 // Now offset is always an integer, suitable for array_slice
286 $offset = $pos !== false ? $pos : count( $this->keys );
287 } else {
288 $offset = (int)$offset;
289 }
290
291 // False means that cannot go back or forward
292 $backwardsOffset = $forwardsOffset = false;
293 // Backwards paging uses numerical indexes, see below
294
295 // Can only skip this if no offset has been provided or the
296 // offset is zero. (offset - limit ) > 1 does not work, because
297 // users can end in offest=2, limit=5 and can't see the first
298 // two messages. That's also why it is capped into zero with
299 // max(). And finally make the offsets to be strings even if
300 // they are numbers in this case.
301 if ( $offset > 0 ) {
302 $backwardsOffset = (string)( max( 0, $offset - $limit ) );
303 }
304
305 // Forwards paging uses keys. If user opens view Untranslated,
306 // translates some messages and then clicks next, the first
307 // message visible in the page is the first message not shown
308 // in the previous page (unless someone else translated it at
309 // the same time). If we used integer offsets, we would skip
310 // same number of messages that were translated, because they
311 // are no longer in the list. For backwards paging this is not
312 // such a big issue, so it still uses integer offsets, because
313 // we would need to also implement "direction" to have it work
314 // correctly.
315 if ( isset( $indexes[$offset + $limit] ) ) {
316 $forwardsOffset = $indexes[$offset + $limit];
317 }
318
319 $this->keys = array_slice( $this->keys, $offset, $limit, true );
320
321 return [ $backwardsOffset, $forwardsOffset, $offset ];
322 }
323
350 public function filter( string $filter, bool $condition, ?int $value = null ): void {
351 if ( !in_array( $filter, self::AVAILABLE_FILTERS, true ) ) {
352 throw new InvalidFilterException( $filter );
353 }
354
355 $keys = $this->keys;
356 if ( $filter === self::FILTER_FUZZY ) {
357 $keys = $this->filterFuzzy( $keys, $condition );
358 } elseif ( $filter === self::FILTER_HAS_TRANSLATION ) {
359 $keys = $this->filterHastranslation( $keys, $condition );
360 } elseif ( $filter === self::FILTER_TRANSLATED ) {
361 $fuzzy = $this->filterFuzzy( $keys, self::INCLUDE_MATCHING );
362 $hastranslation = $this->filterHastranslation( $keys, self::INCLUDE_MATCHING );
363 // Fuzzy messages are not counted as translated messages
364 $translated = $this->filterOnCondition( $hastranslation, $fuzzy );
365 $keys = $this->filterOnCondition( $keys, $translated, $condition );
366 } elseif ( $filter === self::FILTER_CHANGED ) {
367 $keys = $this->filterChanged( $keys, $condition );
368 } elseif ( $filter === self::FILTER_REVIEWER ) {
369 $keys = $this->filterReviewer( $keys, $condition, $value );
370 } elseif ( $filter === self::FILTER_LAST_TRANSLATOR ) {
371 $keys = $this->filterLastTranslator( $keys, $condition, $value );
372 } else {
373 if ( !isset( $this->tags[$filter] ) ) {
374 if ( $filter !== self::FILTER_OPTIONAL && $filter !== self::FILTER_IGNORED ) {
375 throw new RuntimeException( "No tagged messages for custom filter $filter" );
376 }
377 $keys = $this->filterOnCondition( $keys, [], $condition );
378 } else {
379 $taggedKeys = array_flip( $this->tags[$filter] );
380 $keys = $this->filterOnCondition( $keys, $taggedKeys, $condition );
381 }
382 }
383
384 $this->keys = $keys;
385 }
386
388 public function filterUntranslatedOptional(): void {
389 $optionalKeys = array_flip( $this->tags['optional'] ?? [] );
390 // Convert plain message keys to array<string,TitleValue>
391 $optional = $this->filterOnCondition( $this->keys, $optionalKeys, self::INCLUDE_MATCHING );
392 // Then get reduce that list to those which have no translation. Ensure we don't
393 // accidentally populate the info cache with too few keys.
394 $this->loadInfo( $this->keys );
395 $untranslatedOptional = $this->filterHastranslation( $optional, self::EXCLUDE_MATCHING );
396 // Now remove that list from the full list
397 $this->keys = $this->filterOnCondition( $this->keys, $untranslatedOptional );
398 }
399
415 private function filterOnCondition( array $keys, array $condKeys, bool $condition = true ): array {
416 if ( $condition === self::EXCLUDE_MATCHING ) {
417 // Delete $condKeys from $keys
418 foreach ( array_keys( $condKeys ) as $key ) {
419 unset( $keys[$key] );
420 }
421 } else {
422 // Keep the keys which are in $condKeys
423 foreach ( array_keys( $keys ) as $key ) {
424 if ( !isset( $condKeys[$key] ) ) {
425 unset( $keys[$key] );
426 }
427 }
428 }
429
430 return $keys;
431 }
432
440 private function filterFuzzy( array $keys, bool $condition ): array {
441 $this->loadInfo( $keys );
442
443 $origKeys = [];
444 if ( $condition === self::INCLUDE_MATCHING ) {
445 $origKeys = $keys;
446 }
447
448 foreach ( $this->dbInfo as $row ) {
449 if ( $row->rt_type !== null ) {
450 unset( $keys[$this->rowToKey( $row )] );
451 }
452 }
453
454 if ( $condition === self::INCLUDE_MATCHING ) {
455 $keys = array_diff( $origKeys, $keys );
456 }
457
458 return $keys;
459 }
460
468 private function filterHastranslation( array $keys, bool $condition ): array {
469 $this->loadInfo( $keys );
470
471 $origKeys = [];
472 if ( $condition === self::INCLUDE_MATCHING ) {
473 $origKeys = $keys;
474 }
475
476 foreach ( $this->dbInfo as $row ) {
477 unset( $keys[$this->rowToKey( $row )] );
478 }
479
480 // Check also if there is something in the file that is not yet in the database
481 foreach ( array_keys( $this->infile ) as $inf ) {
482 unset( $keys[$inf] );
483 }
484
485 // Remove the messages which do not have a translation from the list
486 if ( $condition === self::INCLUDE_MATCHING ) {
487 $keys = array_diff( $origKeys, $keys );
488 }
489
490 return $keys;
491 }
492
501 private function filterChanged( array $keys, bool $condition ): array {
502 $this->loadData( $keys );
503
504 $origKeys = [];
505 if ( $condition === self::INCLUDE_MATCHING ) {
506 $origKeys = $keys;
507 }
508
509 $revStore = MediaWikiServices::getInstance()->getRevisionStore();
510 $infileRows = [];
511 foreach ( $this->dbData as $row ) {
512 $mkey = $this->rowToKey( $row );
513 if ( isset( $this->infile[$mkey] ) ) {
514 $infileRows[] = $row;
515 }
516 }
517
518 $revisions = $revStore->newRevisionsFromBatch( $infileRows, [
519 'slots' => [ SlotRecord::MAIN ],
520 'content' => true
521 ] )->getValue();
522 foreach ( $infileRows as $row ) {
523 $content = $revisions[$row->rev_id]?->getContent( SlotRecord::MAIN );
524 if ( $content instanceof TextContent ) {
525 $mkey = $this->rowToKey( $row );
526 if ( $this->infile[$mkey] === $content->getText() ) {
527 // Remove unchanged messages from the list
528 unset( $keys[$mkey] );
529 }
530 }
531 }
532
533 // Remove the messages which have changed from the original list
534 if ( $condition === self::INCLUDE_MATCHING ) {
535 $keys = $this->filterOnCondition( $origKeys, $keys );
536 }
537
538 return $keys;
539 }
540
549 private function filterReviewer( array $keys, bool $condition, ?int $userId ): array {
550 $this->loadReviewInfo( $keys );
551 $origKeys = $keys;
552
553 /* This removes messages from the list which have certain
554 * reviewer (among others) */
555 foreach ( $this->dbReviewData as $row ) {
556 if ( $userId === null || (int)$row->trr_user === $userId ) {
557 unset( $keys[$this->rowToKey( $row )] );
558 }
559 }
560
561 if ( $condition === self::INCLUDE_MATCHING ) {
562 $keys = array_diff( $origKeys, $keys );
563 }
564
565 return $keys;
566 }
567
574 private function filterLastTranslator( array $keys, bool $condition, ?int $userId ): array {
575 $this->loadData( $keys );
576 $origKeys = $keys;
577
578 $userId ??= 0;
579 foreach ( $this->dbData as $row ) {
580 if ( (int)$row->rev_user === $userId ) {
581 unset( $keys[$this->rowToKey( $row )] );
582 }
583 }
584
585 if ( $condition === self::INCLUDE_MATCHING ) {
586 $keys = array_diff( $origKeys, $keys );
587 }
588
589 return $keys;
590 }
591
596 private function fixKeys(): array {
597 $newkeys = [];
598
599 $pages = $this->definitions->getPages();
600 foreach ( $pages as $key => $baseTitle ) {
601 $newkeys[$key] = new TitleValue(
602 $baseTitle->getNamespace(),
603 $baseTitle->getDBkey() . '/' . $this->code
604 );
605 }
606
607 return $newkeys;
608 }
609
615 private function loadInfo( array $keys, ?array $titleConds = null ): void {
616 if ( !$this->dbInfo instanceof EmptyIterator ) {
617 return;
618 }
619
620 if ( !count( $keys ) ) {
621 $this->dbInfo = new EmptyIterator();
622 return;
623 }
624
625 $dbr = Utilities::getSafeReadDB();
626
627 $titleConds ??= $this->getTitleConds( $dbr );
628 $iterator = new AppendIterator();
629 foreach ( $titleConds as $conds ) {
630 $queryResults = $dbr->newSelectQueryBuilder()
631 ->select( [ 'page_namespace', 'page_title', 'rt_type' ] )
632 ->from( 'page' )
633 ->leftJoin( 'revtag', null, [
634 'page_id=rt_page',
635 'page_latest=rt_revision',
636 'rt_type' => RevTagStore::FUZZY_TAG,
637 ] )
638 ->where( $conds )
639 ->caller( __METHOD__ )
640 ->fetchResultSet();
641 $iterator->append( $queryResults );
642 }
643
644 $this->dbInfo = $iterator;
645
646 // Populate and cache reverse map now, since if call to initMesages is delayed (e.g. a
647 // filter that calls loadData() is used, or ::slice is used) the reverse map will not
648 // contain all the entries that are present in our $iterator and will throw notices.
649 $this->getReverseMap();
650 }
651
657 private function loadReviewInfo( array $keys, ?array $titleConds = null ): void {
658 if ( !$this->dbReviewData instanceof EmptyIterator ) {
659 return;
660 }
661
662 if ( !count( $keys ) ) {
663 $this->dbReviewData = new EmptyIterator();
664 return;
665 }
666
667 $dbr = Utilities::getSafeReadDB();
668
669 $titleConds ??= $this->getTitleConds( $dbr );
670 $iterator = new AppendIterator();
671 foreach ( $titleConds as $conds ) {
672 $queryResults = $dbr->newSelectQueryBuilder()
673 ->select( [ 'page_namespace', 'page_title', 'trr_user' ] )
674 ->from( 'page' )
675 ->join( 'translate_reviews', null, [ 'page_id=trr_page', 'page_latest=trr_revision' ] )
676 ->where( $conds )
677 ->caller( __METHOD__ )
678 ->fetchResultSet();
679 $iterator->append( $queryResults );
680 }
681
682 $this->dbReviewData = $iterator;
683
684 // Populate and cache reverse map now, since if call to initMesages is delayed (e.g. a
685 // filter that calls loadData() is used, or ::slice is used) the reverse map will not
686 // contain all the entries that are present in our $iterator and will throw notices.
687 $this->getReverseMap();
688 }
689
695 private function loadData( array $keys, ?array $titleConds = null ): void {
696 if ( !$this->dbData instanceof EmptyIterator ) {
697 return;
698 }
699
700 if ( !count( $keys ) ) {
701 $this->dbData = new EmptyIterator();
702 return;
703 }
704
705 $dbr = Utilities::getSafeReadDB();
706 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
707
708 $titleConds ??= $this->getTitleConds( $dbr );
709 $iterator = new AppendIterator();
710 foreach ( $titleConds as $conds ) {
711 $queryResults = $revisionStore->newSelectQueryBuilder( $dbr )
712 ->joinPage()
713 ->joinComment()
714 ->where( $conds )
715 ->andWhere( [ 'page_latest = rev_id' ] )
716 ->caller( __METHOD__ )
717 ->fetchResultSet();
718 $iterator->append( $queryResults );
719 }
720
721 $this->dbData = $iterator;
722
723 // Populate and cache reverse map now, since if call to initMesages is delayed (e.g. a
724 // filter that calls loadData() is used, or ::slice is used) the reverse map will not
725 // contain all the entries that are present in our $iterator and will throw notices.
726 $this->getReverseMap();
727 }
728
733 private function getTitleConds( IReadableDatabase $db ): array {
734 $titles = $this->getTitles();
735 $chunks = array_chunk( $titles, self::MAX_ITEMS_PER_QUERY );
736 $results = [];
737
738 foreach ( $chunks as $titles ) {
739 // Array of array( namespace, pagename )
740 $byNamespace = [];
741 foreach ( $titles as $title ) {
742 $namespace = $title->getNamespace();
743 $pagename = $title->getDBkey();
744 $byNamespace[$namespace][] = $pagename;
745 }
746
747 $conds = [];
748 foreach ( $byNamespace as $namespaces => $pagenames ) {
749 $cond = [
750 'page_namespace' => $namespaces,
751 'page_title' => $pagenames,
752 ];
753
754 $conds[] = $db->makeList( $cond, LIST_AND );
755 }
756
757 $results[] = $db->makeList( $conds, LIST_OR );
758 }
759
760 return $results;
761 }
762
768 private function rowToKey( stdClass $row ): ?string {
769 $map = $this->getReverseMap();
770 if ( isset( $map[$row->page_namespace][$row->page_title] ) ) {
771 return $map[$row->page_namespace][$row->page_title];
772 } else {
773 wfWarn( "Got unknown title from the database: {$row->page_namespace}:{$row->page_title}" );
774
775 return null;
776 }
777 }
778
780 private function getReverseMap(): array {
781 if ( $this->reverseMap !== null ) {
782 return $this->reverseMap;
783 }
784
785 $map = [];
787 foreach ( $this->keys as $mkey => $title ) {
788 $map[$title->getNamespace()][$title->getDBkey()] = $mkey;
789 }
790
791 $this->reverseMap = $map;
792 return $this->reverseMap;
793 }
794
799 public function initMessages(): void {
800 if ( $this->messages !== null ) {
801 return;
802 }
803
804 $messages = [];
805 $definitions = $this->definitions->getDefinitions();
806 $revStore = MediaWikiServices::getInstance()->getRevisionStore();
807 $queryFlags = Utilities::shouldReadFromPrimary() ? IDBAccessObject::READ_LATEST : 0;
808 foreach ( array_keys( $this->keys ) as $mkey ) {
809 $messages[$mkey] = new ThinMessage( $mkey, $definitions[$mkey] );
810 }
811
812 if ( !$this->dbData instanceof EmptyIterator ) {
813 $slotRows = $revStore->getContentBlobsForBatch(
814 $this->dbData,
815 [ SlotRecord::MAIN ],
816 $queryFlags
817 )->getValue();
818
819 foreach ( $this->dbData as $row ) {
820 $mkey = $this->rowToKey( $row );
821 if ( !isset( $messages[$mkey] ) ) {
822 continue;
823 }
824 $messages[$mkey]->setRow( $row );
825 $messages[$mkey]->setProperty( 'revision', $row->page_latest );
826
827 if ( isset( $slotRows[$row->rev_id][SlotRecord::MAIN] ) ) {
828 $slot = $slotRows[$row->rev_id][SlotRecord::MAIN];
829 $messages[$mkey]->setTranslation( $slot->blob_data );
830 }
831 }
832 }
833
834 $fuzzy = [];
835 foreach ( $this->dbInfo as $row ) {
836 if ( $row->rt_type !== null ) {
837 $fuzzy[] = $this->rowToKey( $row );
838 }
839 }
840
841 $this->setTags( 'fuzzy', $fuzzy );
842
843 // Copy tags if any.
844 foreach ( $this->tags as $type => $keys ) {
845 foreach ( $keys as $mkey ) {
846 if ( isset( $messages[$mkey] ) ) {
847 $messages[$mkey]->addTag( $type );
848 }
849 }
850 }
851
852 // Copy infile if any.
853 foreach ( $this->infile as $mkey => $value ) {
854 if ( isset( $messages[$mkey] ) ) {
855 $messages[$mkey]->setInfile( $value );
856 }
857 }
858
859 foreach ( $this->dbReviewData as $row ) {
860 $mkey = $this->rowToKey( $row );
861 if ( !isset( $messages[$mkey] ) ) {
862 continue;
863 }
864 $messages[$mkey]->appendProperty( 'reviewers', $row->trr_user );
865 }
866
867 // Set the status property
868 foreach ( $messages as $obj ) {
869 if ( $obj->hasTag( 'fuzzy' ) ) {
870 $obj->setProperty( 'status', 'fuzzy' );
871 } elseif ( is_array( $obj->getProperty( 'reviewers' ) ) ) {
872 $obj->setProperty( 'status', 'proofread' );
873 } elseif ( $obj->translation() !== null ) {
874 $obj->setProperty( 'status', 'translated' );
875 } else {
876 $obj->setProperty( 'status', 'untranslated' );
877 }
878 }
879
880 $this->messages = $messages;
881 }
882
887 public function offsetExists( $offset ): bool {
888 return isset( $this->keys[$offset] );
889 }
890
892 public function offsetGet( $offset ): ?Message {
893 return $this->messages[$offset] ?? null;
894 }
895
900 public function offsetSet( $offset, $value ): void {
901 $this->messages[$offset] = $value;
902 }
903
905 public function offsetUnset( $offset ): void {
906 unset( $this->keys[$offset] );
907 }
908
915 public function __get( string $name ): void {
916 throw new LogicException( __METHOD__ . ": Trying to access unknown property $name" );
917 }
918
924 public function __set( string $name, $value ): void {
925 throw new LogicException( __METHOD__ . ": Trying to modify unknown property $name" );
926 }
927
933 public function rewind(): void {
934 reset( $this->keys );
935 }
936
938 #[\ReturnTypeWillChange]
939 public function current() {
940 if ( !count( $this->keys ) ) {
941 return false;
942 }
943
944 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
945 return $this->messages[key( $this->keys )];
946 }
947
948 public function key(): string {
949 return key( $this->keys );
950 }
951
952 public function next(): void {
953 next( $this->keys );
954 }
955
956 public function valid(): bool {
957 return isset( $this->messages[key( $this->keys )] );
958 }
959
960 public function count(): int {
961 return count( $this->keys() );
962 }
963
965}
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager($services->getTitleFactory(), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance(LogNames::GROUP_SYNCHRONIZATION), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), $services->getContentLanguageCode() ->toString(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleDependencyPurger'=> static function(MediaWikiServices $services):MessageBundleDependencyPurger { return new MessageBundleDependencyPurger( $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getConnectionProvider());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getConnectionProvider(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):?MessageGroupSubscriptionHookHandler { if(! $services->getExtensionRegistry() ->isLoaded( 'Echo')) { return null;} return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance(LogNames::MAIN), $services->getMainObjectStash(), $services->getConnectionProvider(), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getConnectionProvider(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getConnectionProvider());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getConnectionProvider());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup(), $services->getNamespaceInfo(), $services->getTitleFactory(), $services->getFormatterFactory());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getConnectionProvider(), $services->getObjectCacheFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getConnectionProvider() ->getPrimaryDatabase(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getConnectionProvider(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'), $services->get( 'Translate:MessageGroupSubscription'), $services->getFormatterFactory(), $services->get( 'Translate:HookRunner'),);}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getConnectionProvider(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getConnectionProvider(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getConnectionProvider(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { return new TranslationStashStorage( $services->getConnectionProvider() ->getPrimaryDatabase());}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getConnectionProvider());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getConnectionProvider());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}, 'Translate:WorkflowStatesMessageGroupLoader'=> static function(MediaWikiServices $services):WorkflowStatesMessageGroupLoader { return new WorkflowStatesMessageGroupLoader(new ServiceOptions(WorkflowStatesMessageGroupLoader::CONSTRUCTOR_OPTIONS, $services->getMainConfig()),);},]
@phpcs-require-sorted-array
Class to manage revision tags for translatable bundles.
This file contains the class for core message collections implementation.
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