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