Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
AggregateGroupManager.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
9use MediaWiki\Title\Title;
10use MediaWiki\Title\TitleFactory;
11use MessageGroup;
12use RuntimeException;
14
22
23 private const MESSAGE_GROUP_PREFIX = 'agg-';
24
25 public function __construct(
26 private readonly TitleFactory $titleFactory,
27 private readonly MessageGroupMetadata $messageGroupMetadata,
28 ) {
29 }
30
31 public function supportsAggregation( MessageGroup $group ): bool {
32 return $group instanceof WikiPageMessageGroup || $group instanceof MessageBundleMessageGroup;
33 }
34
35 public function getTargetTitleByGroupId( string $groupId ): Title {
36 $group = MessageGroups::getGroup( $groupId );
37 if ( $group ) {
38 return $this->getTargetTitleByGroup( $group );
39 } else {
40 /* To allow removing no longer existing groups from aggregate message groups,
41 * the message group object $group might not always be available.
42 * In this case we need to fake some title. */
43 return $this->titleFactory->newFromText( "Special:Translate/$groupId" );
44 }
45 }
46
48 public function getTargetTitleByGroup( MessageGroup $group ): Title {
49 $relatedGroupPage = $group->getRelatedPage();
50 if ( !$relatedGroupPage ) {
51 throw new RuntimeException( "No related page found for group " . $group->getId() );
52 }
53
54 return $this->titleFactory->newFromLinkTarget( $relatedGroupPage );
55 }
56
58 public function add( string $name, string $description, ?string $languageCode ): string {
59 // Throw error if the group already exists
60 if ( MessageGroups::labelExists( $name ) ) {
61 throw new DuplicateAggregateGroupException( $name );
62 }
63
64 $aggregateGroupId = $this->generateAggregateGroupId( $name );
65
66 // FIXME: Each call to set runs a DB query. Make this more efficient.
67 $this->messageGroupMetadata->set( $aggregateGroupId, 'name', $name );
68 $this->messageGroupMetadata->set( $aggregateGroupId, 'description', $description );
69 if ( $languageCode ) {
70 // TODO: Add language code validation
71 $this->messageGroupMetadata->set( $aggregateGroupId, 'sourcelanguagecode', $languageCode );
72 }
73 $this->messageGroupMetadata->setSubgroups( $aggregateGroupId, [] );
74
75 return $aggregateGroupId;
76 }
77
86 public function associate( string $aggregateGroupId, array $newSubgroupIds ): array {
87 $existingSubgroupIds = $this->getSubgroups( $aggregateGroupId );
88 $newSubgroups = MessageGroups::getGroupsById( $newSubgroupIds );
89 // Identify groups that do not exist
90 $missingGroupIds = $this->findMissingGroupIds( $newSubgroups, $newSubgroupIds );
91 $invalidGroupIds = [];
92 foreach ( $newSubgroups as $subGroup ) {
93 if ( !$this->supportsAggregation( $subGroup ) ) {
94 $invalidGroupIds[] = $subGroup->getId();
95 }
96 }
97
98 if ( $missingGroupIds || $invalidGroupIds ) {
99 $invalidGroupIds = array_unique( array_merge( $missingGroupIds, $invalidGroupIds ) );
100 throw new AggregateGroupAssociationFailure( $invalidGroupIds );
101 }
102
103 $aggregateGroupLanguage = $this->messageGroupMetadata->get( $aggregateGroupId, 'sourcelanguagecode' );
104 if ( $aggregateGroupLanguage !== false ) {
105 // Ensure that the new groups have the same language as the aggregate group.
106 $sourceLanguageMismatchGroupIds = [];
107 foreach ( $newSubgroups as $subGroup ) {
108 if ( $subGroup->getSourceLanguage() !== $aggregateGroupLanguage ) {
109 $sourceLanguageMismatchGroupIds[] = $subGroup->getId();
110 }
111 }
112
113 if ( $sourceLanguageMismatchGroupIds ) {
114 throw new AggregateGroupLanguageMismatchException(
115 $sourceLanguageMismatchGroupIds,
116 $aggregateGroupLanguage
117 );
118 }
119 }
120
121 $allSubgroupIds = array_unique( array_merge( $existingSubgroupIds, $newSubgroupIds ) );
122 if ( array_diff( $newSubgroupIds, $existingSubgroupIds ) === [] ) {
123 // No new subgroups added
124 return [];
125 }
126
127 $this->messageGroupMetadata->setSubgroups( $aggregateGroupId, $allSubgroupIds );
128 return $newSubgroupIds;
129 }
130
136 public function disassociate( string $aggregateGroupId, array $subgroupIds ): array {
137 $existingSubGroupIds = $this->getSubgroups( $aggregateGroupId );
138 $remainingSubGroupIds = array_diff( $existingSubGroupIds, $subgroupIds );
139 $this->messageGroupMetadata->setSubgroups( $aggregateGroupId, $remainingSubGroupIds );
140 return array_diff( $existingSubGroupIds, $remainingSubGroupIds );
141 }
142
144 public function hasGroupsSupportingAggregation(): bool {
145 $groups = MessageGroups::getAllGroups();
146 foreach ( $groups as $group ) {
147 if ( $this->supportsAggregation( $group ) ) {
148 return true;
149 }
150 }
151
152 return false;
153 }
154
159 public function getAll(): array {
160 $groupsPreload = MessageGroups::getGroupsByType( AggregateMessageGroup::class );
161 $this->messageGroupMetadata->preloadGroups( array_keys( $groupsPreload ), __METHOD__ );
162
163 $groups = MessageGroups::getAllGroups();
164 uasort( $groups, [ MessageGroups::class, 'groupLabelSort' ] );
165 $aggregates = [];
166 foreach ( $groups as $group ) {
167 if ( $group instanceof AggregateMessageGroup ) {
168 // Filter out AggregateGroups configured in YAML
169 $subgroups = $this->messageGroupMetadata->getSubgroups( $group->getId() );
170 if ( $subgroups !== null ) {
171 $aggregates[] = $group;
172 }
173 }
174 }
175
176 return $aggregates;
177 }
178
184 private function findMissingGroupIds( array $subGroups, array $subGroupIds ): array {
185 $existingIds = array_map( static fn ( $group ) => $group->getId(), $subGroups );
186 return array_diff( $subGroupIds, $existingIds );
187 }
188
193 private function getSubgroups( string $aggregateGroupId ): array {
194 $existingSubGroupIds = $this->messageGroupMetadata->getSubgroups( $aggregateGroupId );
195 if ( $existingSubGroupIds !== null ) {
196 // For a newly created aggregate group, it may contain no subgroups, but null
197 // means the group does not exist or something has gone wrong.
198 return $existingSubGroupIds;
199 }
200
201 throw new AggregateGroupNotFoundException( $aggregateGroupId );
202 }
203
204 private function generateAggregateGroupId( string $name ): string {
205 // The database field has a maximum limit of 200 bytes
206 if ( strlen( $name ) + strlen( self::MESSAGE_GROUP_PREFIX ) >= 200 ) {
207 $aggregateGroupId = self::MESSAGE_GROUP_PREFIX . substr( sha1( $name ), 0, 5 );
208 } else {
209 $pattern = '/[\x00-\x1f\x23\x27\x2c\x2e\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/i';
210 $aggregateGroupId = self::MESSAGE_GROUP_PREFIX . preg_replace( $pattern, '_', $name );
211 }
212
213 // ID already exists: Generate a new ID by adding a number to it.
214 $idExists = MessageGroups::getGroup( $aggregateGroupId );
215 if ( $idExists ) {
216 $i = 1;
217 do {
218 $tempId = $aggregateGroupId . '-' . $i;
219 $idExists = MessageGroups::getGroup( $tempId );
220 $i++;
221 } while ( $idExists );
222 $aggregateGroupId = $tempId;
223 }
224
225 return $aggregateGroupId;
226 }
227}
Groups multiple message groups together as one group.
Contains logic to manage aggregate groups and their subgroups.
hasGroupsSupportingAggregation()
Checks if there are any pages that support aggregation.
add(string $name, string $description, ?string $languageCode)
Exception thrown when a duplicate aggregate group with the given name is found.
Factory class for accessing message groups individually by id or all of them as a list.
static getGroup(string $id)
Fetch a message group by id.
Offers functionality for reading and updating Translate group related metadata.
Wraps the translatable page sections into a message group.
Interface for message groups.
getId()
Returns the unique identifier for this group.