Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
AggregateGroupsSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
7use MediaWiki\Cache\LinkBatchFactory;
10use MediaWiki\Html\Html;
11use MediaWiki\SpecialPage\SpecialPage;
12use Xml;
13use XmlSelect;
14
24class AggregateGroupsSpecialPage extends SpecialPage {
25 private bool $hasPermission = false;
26 private LinkBatchFactory $linkBatchFactory;
27 private ?XmlSelect $languageSelector = null;
28 private MessageGroupMetadata $messageGroupMetadata;
29 private AggregateGroupManager $aggregateGroupManager;
30
31 public function __construct(
32 LinkBatchFactory $linkBatchFactory,
33 MessageGroupMetadata $messageGroupMetadata,
34 AggregateGroupManager $aggregateGroupManager
35 ) {
36 parent::__construct( 'AggregateGroups', 'translate-manage' );
37 $this->linkBatchFactory = $linkBatchFactory;
38 $this->messageGroupMetadata = $messageGroupMetadata;
39 $this->aggregateGroupManager = $aggregateGroupManager;
40 }
41
42 protected function getGroupName(): string {
43 return 'translation';
44 }
45
46 public function execute( $parameters ): void {
47 $this->setHeaders();
48 $this->addHelpLink( 'Help:Extension:Translate/Page translation administration' );
49
50 $out = $this->getOutput();
51 $out->addModuleStyles( 'ext.translate.specialpages.styles' );
52
53 // Check permissions
54 if ( $this->getUser()->isAllowed( 'translate-manage' ) ) {
55 $this->hasPermission = true;
56 }
57
58 $groupsPreload = MessageGroups::getGroupsByType( AggregateMessageGroup::class );
59 $this->messageGroupMetadata->preloadGroups( array_keys( $groupsPreload ), __METHOD__ );
60
62 uasort( $groups, [ MessageGroups::class, 'groupLabelSort' ] );
63 $aggregates = [];
64 $pages = [];
65 foreach ( $groups as $group ) {
66 if ( $this->aggregateGroupManager->supportsAggregation( $group ) ) {
67 $pages[] = $group;
68 } elseif ( $group instanceof AggregateMessageGroup ) {
69 // Filter out AggregateGroups configured in YAML
70 $subgroups = $this->messageGroupMetadata->getSubgroups( $group->getId() );
71 if ( $subgroups !== null ) {
72 $aggregates[] = $group;
73 }
74 }
75 }
76
77 if ( !$pages ) {
78 // @todo Use different message
79 $out->addWikiMsg( 'tpt-list-nopages' );
80
81 return;
82 }
83
84 $this->showAggregateGroups( $aggregates );
85 }
86
87 protected function showAggregateGroup( AggregateMessageGroup $group ): string {
88 $id = $group->getId();
89 $label = $group->getLabel();
90 $desc = $group->getDescription( $this->getContext() );
91 $sourceLanguage = $this->messageGroupMetadata->get( $id, 'sourcelanguagecode' );
92
93 $edit = '';
94 $remove = '';
95 $editGroup = '';
96
97 // Add divs for editing Aggregate Groups
98 if ( $this->hasPermission ) {
99 // Group edit and remove buttons
100 $edit = Html::element( 'span', [ 'class' => 'tp-aggregate-edit-ag-button' ] );
101 $remove = Html::element( 'span', [ 'class' => 'tp-aggregate-remove-ag-button' ] );
102
103 // Edit group div
104 $languageSelector = $this->getLanguageSelector( 'edit', $sourceLanguage ?: '-' );
105
106 $editGroupNameLabel = $this->msg( 'tpt-aggregategroup-edit-name' )->escaped();
107 $editGroupName = Html::input(
108 'tp-agg-name',
109 $label,
110 'text',
111 [ 'class' => 'tp-aggregategroup-edit-name', 'maxlength' => '200' ]
112 );
113 $editGroupDescriptionLabel = $this->msg( 'tpt-aggregategroup-edit-description' )->escaped();
114 $editGroupDescription = Html::input(
115 'tp-agg-desc',
116 $desc,
117 'text',
118 [ 'class' => 'tp-aggregategroup-edit-description' ]
119 );
120 $saveButton = Xml::submitButton(
121 $this->msg( 'tpt-aggregategroup-update' )->text(),
122 [ 'class' => 'tp-aggregategroup-update' ]
123 );
124 $cancelButton = Xml::submitButton(
125 $this->msg( 'tpt-aggregategroup-update-cancel' )->text(),
126 [ 'class' => 'tp-aggregategroup-update-cancel' ]
127 );
128 $editGroup = Html::rawElement(
129 'div',
130 [ 'class' => 'tp-edit-group hidden' ],
131 $editGroupNameLabel .
132 $editGroupName .
133 '<br />' .
134 $editGroupDescriptionLabel .
135 $editGroupDescription .
136 '<br />' .
137 $languageSelector .
138 '<br />' .
139 $saveButton .
140 $cancelButton
141 );
142 }
143
144 // Not calling $parent->getGroups() because it has done filtering already
145 $subGroups = $this->messageGroupMetadata->getSubgroups( $id );
146 $shouldExpand = count( $subGroups ) <= 3;
147 $subGroupsId = $this->htmlIdForGroup( $group->getId(), 'tp-subgroup-' );
148
149 // Aggregate Group info div
150 $groupName = Html::rawElement(
151 'h2',
152 [ 'class' => 'tp-name' ],
153 $this->getGroupToggleIcon( $subGroupsId, $shouldExpand ) . htmlspecialchars( $label ) . $edit . $remove
154 );
155 $groupDesc = Html::element(
156 'p',
157 [ 'class' => 'tp-desc' ],
158 $desc
159 );
160 $groupInfo = Html::rawElement(
161 'div',
162 [ 'class' => 'tp-display-group' ],
163 $groupName . $groupDesc
164 );
165
166 $out = Html::openElement(
167 'div',
168 [
169 'class' => 'mw-tpa-group js-mw-tpa-group' . ( $shouldExpand ? ' mw-tpa-group-open' : '' ),
170 'data-groupid' => $id,
171 'data-id' => $this->htmlIdForGroup( $group->getId() )
172 ]
173 );
174 $out .= $groupInfo;
175 $out .= $editGroup;
176 $out .= Html::openElement( 'div', [ 'class' => 'tp-sub-groups', 'id' => $subGroupsId ] );
177 $out .= $this->listSubgroups( $id, $subGroups );
178 $out .= Html::closeElement( 'div' );
179 $out .= '</div>';
180
181 return $out;
182 }
183
185 private function showAggregateGroups( array $aggregates ): void {
186 $out = $this->getOutput();
187 $out->addModules( 'ext.translate.special.aggregategroups' );
188
189 $nojs = Html::errorBox(
190 $this->msg( 'tux-nojs' )->escaped(),
191 '',
192 'tux-nojs'
193 );
194
195 $out->addHTML( $nojs );
196
197 // Add new group if user has permissions
198 if ( $this->hasPermission ) {
199 $out->addHTML(
200 "<a class='tpt-add-new-group' href='#'>" .
201 $this->msg( 'tpt-aggregategroup-add-new' )->escaped() .
202 '</a>'
203 );
204 $languageSelector = $this->getLanguageSelector( 'add', '-' );
205 $newGroupNameLabel = $this->msg( 'tpt-aggregategroup-new-name' )->escaped();
206 $newGroupName = Html::element( 'input', [ 'class' => 'tp-aggregategroup-add-name', 'maxlength' => '200' ] );
207 $newGroupDescriptionLabel = $this->msg( 'tpt-aggregategroup-new-description' )->escaped();
208 $newGroupDescription = Html::element( 'input', [ 'class' => 'tp-aggregategroup-add-description' ] );
209 $saveButton = Html::element(
210 'input',
211 [
212 'type' => 'button',
213 'value' => $this->msg( 'tpt-aggregategroup-save' )->text(),
214 'id' => 'tpt-aggregategroups-save',
215 'class' => 'tp-aggregate-save-button'
216 ]
217 );
218 $closeButton = Html::element(
219 'input',
220 [
221 'type' => 'button',
222 'value' => $this->msg( 'tpt-aggregategroup-close' )->text(),
223 'id' => 'tpt-aggregategroups-close'
224 ]
225 );
226 $newGroupDiv = Html::rawElement(
227 'div',
228 [ 'class' => 'tpt-add-new-group hidden' ],
229 "$newGroupNameLabel $newGroupName<br />" .
230 "$newGroupDescriptionLabel $newGroupDescription<br />" .
231 "$languageSelector <br />"
232 . $saveButton
233 . $closeButton
234 );
235 $out->addHTML( $newGroupDiv );
236 }
237
238 $out->addHTML( Html::openElement( 'div', [ 'class' => 'mw-tpa-groups' ] ) );
239 foreach ( $aggregates as $group ) {
240 $out->addHTML( $this->showAggregateGroup( $group ) );
241 }
242 $out->addHTML( Html::closeElement( 'div' ) );
243 }
244
245 private function listSubgroups( string $groupId, array $subGroupIds ): string {
246 $id = $this->htmlIdForGroup( $groupId, 'mw-tpa-grouplist-' );
247 $out = Html::openElement( 'ol', [ 'id' => $id ] );
248
249 // Get the respective groups and sort them
250 $subgroups = MessageGroups::getGroupsById( $subGroupIds );
251 uasort( $subgroups, [ MessageGroups::class, 'groupLabelSort' ] );
252
253 // Avoid potentially thousands of separate database queries from LinkRenderer::makeKnownLink
254 $groupCache = [];
255 $lb = $this->linkBatchFactory->newLinkBatch();
256 foreach ( $subgroups as $group ) {
257 $subGroupId = $group->getId();
258 $groupCache[ $subGroupId ] = $this->aggregateGroupManager->getTargetTitleByGroupId( $subGroupId );
259 $lb->addObj( $groupCache[ $subGroupId ] );
260 }
261 $lb->setCaller( __METHOD__ );
262 $lb->execute();
263
264 // Add missing invalid group ids back, not returned by getGroupsById
265 foreach ( $subGroupIds as $id ) {
266 if ( !isset( $subgroups[$id] ) ) {
267 $subgroups[$id] = null;
268 }
269 }
270
271 foreach ( $subgroups as $id => $group ) {
272 $remove = '';
273 if ( $this->hasPermission ) {
274 $remove = Html::element(
275 'span',
276 [ 'class' => 'tp-aggregate-remove-button', 'data-groupid' => $id ]
277 );
278 }
279
280 if ( $group ) {
281 $text = $this->getLinkRenderer()->makeKnownLink( $groupCache[ $group->getId() ], $group->getLabel() );
282 $note = htmlspecialchars( MessageGroups::getPriority( $id ) );
283 } else {
284 $text = htmlspecialchars( $id );
285 $note = $this->msg( 'tpt-aggregategroup-invalid-group' )->escaped();
286 }
287
288 $out .= Html::rawElement( 'li', [], "$text$remove $note" );
289 }
290 $out .= Html::closeElement( 'ol' );
291
292 return $out;
293 }
294
295 private function htmlIdForGroup( string $groupId, string $prefix = '' ): string {
296 $id = sha1( $groupId );
297 $id = substr( $id, 5, 8 );
298
299 return $prefix . $id;
300 }
301
302 private function getGroupToggleIcon( string $targetElementId, bool $shouldExpand ): string {
303 if ( $shouldExpand ) {
304 $title = $this->msg( 'tpt-aggregategroup-collapse-group' )->plain();
305 } else {
306 $title = $this->msg( 'tpt-aggregategroup-expand-group' )->plain();
307 }
308
309 return Html::rawElement(
310 'button',
311 [
312 'type' => 'button',
313 'title' => $title,
314 'class' => 'js-tp-toggle-groups tp-toggle-group-icon',
315 'aria-expanded' => $shouldExpand ? 'true' : 'false',
316 'aria-controls' => $targetElementId
317 ]
318 );
319 }
320
321 private function getLanguageSelector( string $action, string $languageToSelect ): string {
322 if ( $this->languageSelector == null ) {
323 // This should be set according to UI language
324 $this->languageSelector = Utilities::getLanguageSelector(
325 $this->getContext()->getLanguage()->getCode(),
326 '-'
327 );
328 }
329
330 $this->languageSelector->setAttribute( 'class', "tp-aggregategroup-$action-source-language" );
331 $this->languageSelector->setDefault( $languageToSelect );
332 $selector = $this->languageSelector->getHTML();
333
334 $languageSelectorLabel = $this->msg( 'tpt-aggregategroup-select-source-language' )->escaped();
335 return $languageSelectorLabel . $selector;
336 }
337}
Groups multiple message groups together as one group.
Contains logic to store, validate, fetch aggregate groups created via Special:AggregateGroups.
static getPriority( $group)
We want to de-emphasize time sensitive groups like news for 2009.
static getGroupsByType(string $type)
Get only groups of specific type (class).
static getGroupsById(array $ids, bool $skipMeta=false)
Get message groups for corresponding message group ids.
Offers functionality for reading and updating Translate group related metadata.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31
getDescription(IContextSource $context=null)
Returns a longer description about the group.
getLabel(IContextSource $context=null)
Returns the human readable label (as plain text).
getId()
Returns the unique identifier for this group.