Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
AggregateGroupsSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
7use Html;
8use MediaWiki\Cache\LinkBatchFactory;
10use SpecialPage;
13use Xml;
14
24class AggregateGroupsSpecialPage extends SpecialPage {
26 private $hasPermission = false;
28 private $linkBatchFactory;
29
30 public function __construct( LinkBatchFactory $linkBatchFactory ) {
31 parent::__construct( 'AggregateGroups', 'translate-manage' );
32 $this->linkBatchFactory = $linkBatchFactory;
33 }
34
35 protected function getGroupName(): string {
36 return 'translation';
37 }
38
39 public function execute( $parameters ) {
40 $this->setHeaders();
41 $this->addHelpLink( 'Help:Extension:Translate/Page translation administration' );
42
43 $out = $this->getOutput();
44 $out->addModuleStyles( 'ext.translate.specialpages.styles' );
45
46 // Check permissions
47 if ( $this->getUser()->isAllowed( 'translate-manage' ) ) {
48 $this->hasPermission = true;
49 }
50
51 $groupsPreload = MessageGroups::getGroupsByType( AggregateMessageGroup::class );
52 TranslateMetadata::preloadGroups( array_keys( $groupsPreload ), __METHOD__ );
53
54 $groups = MessageGroups::getAllGroups();
55 uasort( $groups, [ MessageGroups::class, 'groupLabelSort' ] );
56 $aggregates = [];
57 $pages = [];
58 foreach ( $groups as $group ) {
59 if ( $group instanceof WikiPageMessageGroup ) {
60 $pages[] = $group;
61 } elseif ( $group instanceof AggregateMessageGroup ) {
62 // Filter out AggregateGroups configured in YAML
63 $subgroups = TranslateMetadata::getSubgroups( $group->getId() );
64 if ( $subgroups !== null ) {
65 $aggregates[] = $group;
66 }
67 }
68 }
69
70 if ( !$pages ) {
71 // @todo Use different message
72 $out->addWikiMsg( 'tpt-list-nopages' );
73
74 return;
75 }
76
77 $this->showAggregateGroups( $aggregates );
78 }
79
80 protected function showAggregateGroup( AggregateMessageGroup $group ): string {
81 $id = $group->getId();
82 $label = $group->getLabel();
83 $desc = $group->getDescription( $this->getContext() );
84
85 $edit = '';
86 $remove = '';
87 $editGroup = '';
88 $select = '';
89 $addButton = '';
90
91 // Add divs for editing Aggregate Groups
92 if ( $this->hasPermission ) {
93 // Group edit and remove buttons
94 $edit = Html::element( 'span', [ 'class' => 'tp-aggregate-edit-ag-button' ] );
95 $remove = Html::element( 'span', [ 'class' => 'tp-aggregate-remove-ag-button' ] );
96
97 // Edit group div
98 $editGroupNameLabel = $this->msg( 'tpt-aggregategroup-edit-name' )->escaped();
99 $editGroupName = Html::input(
100 'tp-agg-name',
101 $label,
102 'text',
103 [ 'class' => 'tp-aggregategroup-edit-name', 'maxlength' => '200' ]
104 );
105 $editGroupDescriptionLabel = $this->msg( 'tpt-aggregategroup-edit-description' )->escaped();
106 $editGroupDescription = Html::input(
107 'tp-agg-desc',
108 $desc,
109 'text',
110 [ 'class' => 'tp-aggregategroup-edit-description' ]
111 );
112 $saveButton = Xml::submitButton(
113 $this->msg( 'tpt-aggregategroup-update' )->text(),
114 [ 'class' => 'tp-aggregategroup-update' ]
115 );
116 $cancelButton = Xml::submitButton(
117 $this->msg( 'tpt-aggregategroup-update-cancel' )->text(),
118 [ 'class' => 'tp-aggregategroup-update-cancel' ]
119 );
120 $editGroup = Html::rawElement(
121 'div',
122 [ 'class' => 'tp-edit-group hidden' ],
123 $editGroupNameLabel .
124 $editGroupName .
125 '<br />' .
126 $editGroupDescriptionLabel .
127 $editGroupDescription .
128 $saveButton .
129 $cancelButton
130 );
131
132 // Subgroups selector
133 $select = Html::input( 'tp-subgroups-input', '', 'text', [ 'class' => 'tp-group-input' ] );
134 $addButton = Html::element( 'input',
135 [
136 'type' => 'button',
137 'value' => $this->msg( 'tpt-aggregategroup-add' )->text(),
138 'class' => 'tp-aggregate-add-button'
139 ]
140 );
141 }
142
143 // Not calling $parent->getGroups() because it has done filtering already
144 $subGroups = TranslateMetadata::getSubgroups( $id );
145 $shouldExpand = count( $subGroups ) <= 3;
146 $subGroupsId = $this->htmlIdForGroup( $group->getId(), 'tp-subgroup-' );
147
148 // Aggregate Group info div
149 $groupName = Html::rawElement(
150 'h2',
151 [ 'class' => 'tp-name' ],
152 $this->getGroupToggleIcon( $subGroupsId, $shouldExpand ) . htmlspecialchars( $label ) . $edit . $remove
153 );
154 $groupDesc = Html::element(
155 'p',
156 [ 'class' => 'tp-desc' ],
157 $desc
158 );
159 $groupInfo = Html::rawElement(
160 'div',
161 [ 'class' => 'tp-display-group' ],
162 $groupName . $groupDesc
163 );
164
165 $out = Html::openElement(
166 'div',
167 [
168 'class' => 'mw-tpa-group js-mw-tpa-group' . ( $shouldExpand ? ' mw-tpa-group-open' : '' ),
169 'data-groupid' => $id,
170 'data-id' => $this->htmlIdForGroup( $group->getId() )
171 ]
172 );
173 $out .= $groupInfo;
174 $out .= $editGroup;
175 $out .= Html::openElement( 'div', [ 'class' => 'tp-sub-groups', 'id' => $subGroupsId ] );
176 $out .= $this->listSubgroups( $id, $subGroups );
177 $out .= $select . $addButton;
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' )->plain(),
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 $newGroupNameLabel = $this->msg( 'tpt-aggregategroup-new-name' )->escaped();
205 $newGroupName = Html::element( 'input', [ 'class' => 'tp-aggregategroup-add-name', 'maxlength' => '200' ] );
206 $newGroupDescriptionLabel = $this->msg( 'tpt-aggregategroup-new-description' )->escaped();
207 $newGroupDescription = Html::element( 'input', [ 'class' => 'tp-aggregategroup-add-description' ] );
208 $saveButton = Html::element(
209 'input',
210 [
211 'type' => 'button',
212 'value' => $this->msg( 'tpt-aggregategroup-save' )->text(),
213 'id' => 'tpt-aggregategroups-save',
214 'class' => 'tp-aggregate-save-button'
215 ]
216 );
217 $closeButton = Html::element(
218 'input',
219 [
220 'type' => 'button',
221 'value' => $this->msg( 'tpt-aggregategroup-close' )->text(),
222 'id' => 'tpt-aggregategroups-close'
223 ]
224 );
225 $newGroupDiv = Html::rawElement(
226 'div',
227 [ 'class' => 'tpt-add-new-group hidden' ],
228 "$newGroupNameLabel $newGroupName<br />" .
229 "$newGroupDescriptionLabel $newGroupDescription<br />$saveButton $closeButton"
230 );
231 $out->addHTML( $newGroupDiv );
232 }
233
235 foreach ( $aggregates as $group ) {
236 $out->addHTML( $this->showAggregateGroup( $group ) );
237 }
238 }
239
240 private function listSubgroups( string $groupId, array $subGroupIds ): string {
241 $id = $this->htmlIdForGroup( $groupId, 'mw-tpa-grouplist-' );
242 $out = Html::openElement( 'ol', [ 'id' => $id ] );
243
244 // Get the respective groups and sort them
245 $subgroups = MessageGroups::getGroupsById( $subGroupIds );
246 '@phan-var WikiPageMessageGroup[] $subgroups';
247 uasort( $subgroups, [ MessageGroups::class, 'groupLabelSort' ] );
248
249 // Avoid potentially thousands of separate database queries
250 $lb = $this->linkBatchFactory->newLinkBatch();
251 foreach ( $subgroups as $group ) {
252 $lb->addObj( $group->getTitle() );
253 }
254 $lb->setCaller( __METHOD__ );
255 $lb->execute();
256
257 // Add missing invalid group ids back, not returned by getGroupsById
258 foreach ( $subGroupIds as $id ) {
259 if ( !isset( $subgroups[$id] ) ) {
260 $subgroups[$id] = null;
261 }
262 }
263
264 foreach ( $subgroups as $id => $group ) {
265 $remove = '';
266 if ( $this->hasPermission ) {
267 $remove = Html::element(
268 'span',
269 [ 'class' => 'tp-aggregate-remove-button', 'data-groupid' => $id ]
270 );
271 }
272
273 if ( $group ) {
274 $text = $this->getLinkRenderer()->makeKnownLink( $group->getTitle() );
275 $note = htmlspecialchars( MessageGroups::getPriority( $id ) );
276 } else {
277 $text = htmlspecialchars( $id );
278 $note = $this->msg( 'tpt-aggregategroup-invalid-group' )->escaped();
279 }
280
281 $out .= Html::rawElement( 'li', [], "$text$remove $note" );
282 }
283 $out .= Html::closeElement( 'ol' );
284
285 return $out;
286 }
287
288 private function htmlIdForGroup( string $groupId, string $prefix = '' ): string {
289 $id = sha1( $groupId );
290 $id = substr( $id, 5, 8 );
291
292 return $prefix . $id;
293 }
294
295 private function getGroupToggleIcon( string $targetElementId, bool $shouldExpand ): string {
296 if ( $shouldExpand ) {
297 $title = $this->msg( 'tpt-aggregategroup-collapse-group' )->plain();
298 } else {
299 $title = $this->msg( 'tpt-aggregategroup-expand-group' )->plain();
300 }
301
302 return Html::rawElement(
303 'button',
304 [
305 'type' => 'button',
306 'title' => $title,
307 'class' => 'js-tp-toggle-groups tp-toggle-group-icon',
308 'aria-expanded' => $shouldExpand ? 'true' : 'false',
309 'aria-controls' => $targetElementId
310 ]
311 );
312 }
313}
Groups multiple message groups together as one group.
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.
Factory class for accessing message groups individually by id or all of them as an list.
Wraps the translatable page sections into a message group.