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