Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
14.18% |
40 / 282 |
|
0.00% |
0 / 22 |
CRAP | |
0.00% |
0 / 1 |
MessageWebImporter | |
14.18% |
40 / 282 |
|
0.00% |
0 / 22 |
3077.83 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setUser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setGroup | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getCode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setCode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doHeader | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
doFooter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
allowProcess | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getActions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
execute | |
0.00% |
0 / 137 |
|
0.00% |
0 / 1 |
650 | |||
doAction | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
110 | |||
checkProcessTime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doImport | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
doFuzzy | |
86.96% |
40 / 46 |
|
0.00% |
0 / 1 |
8.14 | |||
makeTranslationTitle | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
makeSectionElement | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
escapeNameForPHP | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\Synchronization; |
5 | |
6 | use DifferenceEngine; |
7 | use InvalidArgumentException; |
8 | use MediaWiki\CommentStore\CommentStoreComment; |
9 | use MediaWiki\Content\ContentHandler; |
10 | use MediaWiki\Context\RequestContext; |
11 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups; |
12 | use MediaWiki\Extension\Translate\MessageLoading\MessageCollection; |
13 | use MediaWiki\Extension\Translate\MessageLoading\MessageHandle; |
14 | use MediaWiki\Extension\Translate\SystemUsers\FuzzyBot; |
15 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
16 | use MediaWiki\Html\Html; |
17 | use MediaWiki\Language\Language; |
18 | use MediaWiki\MediaWikiServices; |
19 | use MediaWiki\Revision\MutableRevisionRecord; |
20 | use MediaWiki\Revision\SlotRecord; |
21 | use MediaWiki\Title\Title; |
22 | use MediaWiki\User\User; |
23 | use MediaWiki\Xml\Xml; |
24 | use MessageGroup; |
25 | use MessageLocalizer; |
26 | use RecentChange; |
27 | use RuntimeException; |
28 | |
29 | /** |
30 | * Class which encapsulates message importing. It scans for changes (new, changed, deleted), |
31 | * displays them in pretty way with diffs and finally executes the actions the user choices. |
32 | * |
33 | * @author Niklas Laxström |
34 | * @author Siebrand Mazeland |
35 | * @copyright Copyright © 2009-2013, Niklas Laxström, Siebrand Mazeland |
36 | * @license GPL-2.0-or-later |
37 | */ |
38 | class MessageWebImporter { |
39 | private Title $title; |
40 | private User $user; |
41 | private MessageGroup $group; |
42 | private string $code; |
43 | /** @var int|null */ |
44 | private $time; |
45 | private MessageLocalizer $messageLocalizer; |
46 | /** Maximum processing time in seconds. */ |
47 | private const MAX_PROCESSING_TIME = 43; |
48 | |
49 | /** |
50 | * @param Title $title |
51 | * @param User $user |
52 | * @param MessageLocalizer $messageLocalizer |
53 | * @param MessageGroup|string|null $group |
54 | * @param string $code |
55 | */ |
56 | public function __construct( |
57 | Title $title, |
58 | User $user, |
59 | MessageLocalizer $messageLocalizer, |
60 | $group = null, |
61 | string $code = 'en' |
62 | ) { |
63 | $this->setTitle( $title ); |
64 | $this->setUser( $user ); |
65 | $this->setGroup( $group ); |
66 | $this->setCode( $code ); |
67 | $this->messageLocalizer = $messageLocalizer; |
68 | } |
69 | |
70 | /** Wrapper for consistency with SpecialPage */ |
71 | public function getTitle(): Title { |
72 | return $this->title; |
73 | } |
74 | |
75 | public function setTitle( Title $title ): void { |
76 | $this->title = $title; |
77 | } |
78 | |
79 | public function getUser(): User { |
80 | return $this->user; |
81 | } |
82 | |
83 | public function setUser( User $user ): void { |
84 | $this->user = $user; |
85 | } |
86 | |
87 | public function getGroup(): MessageGroup { |
88 | return $this->group; |
89 | } |
90 | |
91 | /** @param MessageGroup|string $group MessageGroup object or group ID */ |
92 | public function setGroup( $group ): void { |
93 | if ( $group instanceof MessageGroup ) { |
94 | $this->group = $group; |
95 | } else { |
96 | $this->group = MessageGroups::getGroup( $group ); |
97 | } |
98 | } |
99 | |
100 | public function getCode(): string { |
101 | return $this->code; |
102 | } |
103 | |
104 | public function setCode( string $code = 'en' ): void { |
105 | $this->code = $code; |
106 | } |
107 | |
108 | protected function getAction(): string { |
109 | return $this->getTitle()->getLocalURL(); |
110 | } |
111 | |
112 | protected function doHeader(): string { |
113 | $formParams = [ |
114 | 'method' => 'post', |
115 | 'action' => $this->getAction(), |
116 | 'class' => 'mw-translate-manage' |
117 | ]; |
118 | |
119 | $csrfTokenSet = RequestContext::getMain()->getCsrfTokenSet(); |
120 | return Xml::openElement( 'form', $formParams ) . |
121 | Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . |
122 | Html::hidden( 'token', $csrfTokenSet->getToken() ) . |
123 | Html::hidden( 'process', 1 ); |
124 | } |
125 | |
126 | protected function doFooter(): string { |
127 | return '</form>'; |
128 | } |
129 | |
130 | protected function allowProcess(): bool { |
131 | $context = RequestContext::getMain(); |
132 | $request = $context->getRequest(); |
133 | $csrfTokenSet = $context->getCsrfTokenSet(); |
134 | |
135 | return $request->wasPosted() |
136 | && $request->getBool( 'process' ) |
137 | && $csrfTokenSet->matchTokenField( 'token' ); |
138 | } |
139 | |
140 | protected function getActions(): array { |
141 | return [ |
142 | 'import', |
143 | $this->code === 'en' ? 'fuzzy' : 'conflict', |
144 | 'ignore', |
145 | ]; |
146 | } |
147 | |
148 | public function execute( array $messages ): bool { |
149 | $context = RequestContext::getMain(); |
150 | $output = $context->getOutput(); |
151 | |
152 | // Set up diff engine |
153 | $diff = new DifferenceEngine(); |
154 | $diff->showDiffStyle(); |
155 | $diff->setReducedLineNumbers(); |
156 | |
157 | // Check whether we do processing |
158 | $process = $this->allowProcess(); |
159 | |
160 | // Initialise collection |
161 | $group = $this->getGroup(); |
162 | $code = $this->getCode(); |
163 | $collection = $group->initCollection( $code ); |
164 | $collection->loadTranslations(); |
165 | |
166 | $output->addHTML( $this->doHeader() ); |
167 | |
168 | // Initialise variable to keep track whether all changes were imported |
169 | // or not. If we're allowed to process, initially assume they were. |
170 | $allDone = $process; |
171 | |
172 | // Determine changes for each message. |
173 | $changed = []; |
174 | |
175 | foreach ( $messages as $key => $value ) { |
176 | $old = null; |
177 | $isExistingMessageFuzzy = false; |
178 | |
179 | if ( isset( $collection[$key] ) ) { |
180 | // This returns null if no existing translation is found |
181 | $old = $collection[$key]->translation(); |
182 | $isExistingMessageFuzzy = $collection[$key]->hasTag( 'fuzzy' ); |
183 | } |
184 | |
185 | if ( $old === null ) { |
186 | // We found a new translation for this message of the |
187 | // current group: import it. |
188 | if ( $process ) { |
189 | $action = 'import'; |
190 | $this->doAction( |
191 | $action, |
192 | $group, |
193 | $key, |
194 | $value |
195 | ); |
196 | } |
197 | // Show the user that we imported the new translation |
198 | $para = '<code class="mw-tmi-new">' . htmlspecialchars( $key ) . '</code>'; |
199 | $name = $context->msg( 'translate-manage-import-new' )->rawParams( $para ) |
200 | ->escaped(); |
201 | $text = Utilities::convertWhiteSpaceToHTML( $value ); |
202 | $changed[] = self::makeSectionElement( $name, 'new', $text ); |
203 | } else { |
204 | // No changes at all, ignore |
205 | if ( $old === (string)$value ) { |
206 | continue; |
207 | } |
208 | |
209 | // Check if the message is already fuzzy in the system, and then determine if there are changes |
210 | $oldTextForDiff = $old; |
211 | if ( $isExistingMessageFuzzy ) { |
212 | if ( MessageHandle::makeFuzzyString( $old ) === (string)$value ) { |
213 | continue; |
214 | } |
215 | |
216 | // Normalize the display of FUZZY message diffs so that if an old message has |
217 | // a fuzzy tag, then that is added to the text used in the diff. |
218 | $oldTextForDiff = MessageHandle::makeFuzzyString( $old ); |
219 | } |
220 | |
221 | // MutableRevisionRecord expects a page that can exist, so use a dummy non-special page. |
222 | $dummyMainPage = Title::makeTitle( NS_MAIN, 'Some title just for diff' ); |
223 | $oldContent = ContentHandler::makeContent( $oldTextForDiff, $dummyMainPage ); |
224 | $oldRevision = new MutableRevisionRecord( $dummyMainPage ); |
225 | $oldRevision->setContent( SlotRecord::MAIN, $oldContent ); |
226 | |
227 | $newContent = ContentHandler::makeContent( $value, $dummyMainPage ); |
228 | $newRevision = new MutableRevisionRecord( $dummyMainPage ); |
229 | $newRevision->setContent( SlotRecord::MAIN, $newContent ); |
230 | |
231 | $diff->setRevisions( $oldRevision, $newRevision ); |
232 | $text = $diff->getDiff( '', '' ); |
233 | |
234 | // This is a changed translation. Note it for the next steps. |
235 | $type = 'changed'; |
236 | |
237 | // Get the user instructions for the current message, |
238 | // submitted together with the form |
239 | $action = $context->getRequest() |
240 | ->getVal( self::escapeNameForPHP( "action-$type-$key" ) ); |
241 | |
242 | if ( $process ) { |
243 | if ( $changed === [] ) { |
244 | // Initialise the HTML list showing the changes performed |
245 | $changed[] = '<ul>'; |
246 | } |
247 | |
248 | if ( $action === null ) { |
249 | // We have been told to process the messages, but not |
250 | // what to do with this one. Tell the user. |
251 | $message = $context->msg( |
252 | 'translate-manage-inconsistent', |
253 | wfEscapeWikiText( "action-$type-$key" ) |
254 | )->parse(); |
255 | $changed[] = "<li>$message</li></ul>"; |
256 | |
257 | // Also stop any further processing for the other messages. |
258 | $process = false; |
259 | } else { |
260 | // Check processing time |
261 | if ( $this->time === null ) { |
262 | $this->time = (int)wfTimestamp(); |
263 | } |
264 | |
265 | // We have all the necessary information on this changed |
266 | // translation: actually process the message |
267 | $messageKeyAndParams = $this->doAction( |
268 | $action, |
269 | $group, |
270 | $key, |
271 | $value |
272 | ); |
273 | |
274 | // Show what we just did, adding to the list of changes |
275 | $msgKey = array_shift( $messageKeyAndParams ); |
276 | $params = $messageKeyAndParams; |
277 | $message = $context->msg( $msgKey, $params )->parse(); |
278 | $changed[] = "<li>$message</li>"; |
279 | |
280 | // Stop processing further messages if too much time |
281 | // has been spent. |
282 | if ( $this->checkProcessTime() ) { |
283 | $process = false; |
284 | $message = $context->msg( 'translate-manage-toolong' ) |
285 | ->numParams( self::MAX_PROCESSING_TIME )->parse(); |
286 | $changed[] = "<li>$message</li></ul>"; |
287 | } |
288 | |
289 | continue; |
290 | } |
291 | } |
292 | |
293 | // We are not processing messages, or no longer, or this was an |
294 | // un-actionable translation. We will eventually return false |
295 | $allDone = false; |
296 | |
297 | // Prepare to ask the user what to do with this message |
298 | $actions = $this->getActions(); |
299 | $defaultAction = $action ?: 'import'; |
300 | |
301 | $act = []; |
302 | |
303 | // Give grep a chance to find the usages: |
304 | // translate-manage-action-import, translate-manage-action-conflict, |
305 | // translate-manage-action-ignore, translate-manage-action-fuzzy |
306 | foreach ( $actions as $action ) { |
307 | $label = $context->msg( "translate-manage-action-$action" )->escaped(); |
308 | $act[] = Html::rawElement( |
309 | 'label', |
310 | [], |
311 | Html::radio( |
312 | self::escapeNameForPHP( "action-$type-$key" ), |
313 | $action === $defaultAction, |
314 | [ 'value' => $action ] |
315 | ) . |
316 | "\u{00A0}" . |
317 | $label |
318 | ); |
319 | } |
320 | |
321 | $param = '<code class="mw-tmi-diff">' . htmlspecialchars( $key ) . '</code>'; |
322 | $name = $context->msg( 'translate-manage-import-diff' ) |
323 | ->rawParams( $param, implode( ' ', $act ) ) |
324 | ->escaped(); |
325 | |
326 | $changed[] = self::makeSectionElement( $name, $type, $text ); |
327 | } |
328 | } |
329 | |
330 | if ( !$process ) { |
331 | $collection->filter( MessageCollection::FILTER_HAS_TRANSLATION, MessageCollection::INCLUDE_MATCHING ); |
332 | $keys = $collection->getMessageKeys(); |
333 | |
334 | $diff = array_diff( $keys, array_keys( $messages ) ); |
335 | |
336 | foreach ( $diff as $s ) { |
337 | $para = '<code class="mw-tmi-deleted">' . htmlspecialchars( $s ) . '</code>'; |
338 | $name = $context->msg( 'translate-manage-import-deleted' )->rawParams( $para )->escaped(); |
339 | $text = Utilities::convertWhiteSpaceToHTML( $collection[$s]->translation() ); |
340 | $changed[] = self::makeSectionElement( $name, 'deleted', $text ); |
341 | } |
342 | } |
343 | |
344 | if ( $process || ( $changed === [] && $code !== 'en' ) ) { |
345 | if ( $changed === [] ) { |
346 | $output->addWikiMsg( 'translate-manage-nochanges-other' ); |
347 | } |
348 | |
349 | if ( $changed === [] || !str_starts_with( end( $changed ), '<li>' ) ) { |
350 | $changed[] = '<ul>'; |
351 | } |
352 | |
353 | $changed[] = '</ul>'; |
354 | |
355 | $languageName = Utilities::getLanguageName( $code, $context->getLanguage()->getCode() ); |
356 | $message = $context |
357 | ->msg( 'translate-manage-import-done', $group->getId(), $group->getLabel(), $languageName ) |
358 | ->parse(); |
359 | $changed[] = Html::successBox( $message ); |
360 | $output->addHTML( implode( "\n", $changed ) ); |
361 | } else { |
362 | // END |
363 | if ( $changed !== [] ) { |
364 | if ( $code === 'en' ) { |
365 | $output->addWikiMsg( 'translate-manage-intro-en' ); |
366 | } else { |
367 | $lang = Utilities::getLanguageName( |
368 | $code, |
369 | $context->getLanguage()->getCode() |
370 | ); |
371 | $output->addWikiMsg( 'translate-manage-intro-other', $lang ); |
372 | } |
373 | $output->addHTML( Html::hidden( 'language', $code ) ); |
374 | $output->addHTML( implode( "\n", $changed ) ); |
375 | $output->addHTML( Html::submitButton( $context->msg( 'translate-manage-submit' )->text() ) ); |
376 | } else { |
377 | $output->addWikiMsg( 'translate-manage-nochanges' ); |
378 | } |
379 | } |
380 | |
381 | $output->addHTML( $this->doFooter() ); |
382 | |
383 | return $allDone; |
384 | } |
385 | |
386 | /** |
387 | * Perform an action on a given group/key/code |
388 | * |
389 | * @param string $action Options: 'import', 'conflict' or 'ignore' |
390 | * @param MessageGroup $group |
391 | * @param string $key Message key |
392 | * @param string $message Contents for the $key/code combination |
393 | * @return array Action result |
394 | */ |
395 | private function doAction( |
396 | string $action, |
397 | MessageGroup $group, |
398 | string $key, |
399 | string $message |
400 | ): array { |
401 | global $wgTranslateDocumentationLanguageCode; |
402 | |
403 | $comment = ''; |
404 | $code = $this->getCode(); |
405 | $title = $this->makeTranslationTitle( $group, $key, $code ); |
406 | |
407 | if ( $action === 'import' || $action === 'conflict' ) { |
408 | if ( $action === 'import' ) { |
409 | $comment = wfMessage( 'translate-manage-import-summary' )->inContentLanguage()->plain(); |
410 | } else { |
411 | $comment = wfMessage( 'translate-manage-conflict-summary' )->inContentLanguage()->plain(); |
412 | $message = MessageHandle::makeFuzzyString( $message ); |
413 | } |
414 | |
415 | return self::doImport( $title, $message, $comment, $this->getUser(), $this->messageLocalizer ); |
416 | } elseif ( $action === 'ignore' ) { |
417 | return [ 'translate-manage-import-ignore', $key ]; |
418 | } elseif ( $action === 'fuzzy' && $code !== 'en' && |
419 | $code !== $wgTranslateDocumentationLanguageCode |
420 | ) { |
421 | $message = MessageHandle::makeFuzzyString( $message ); |
422 | |
423 | return self::doImport( $title, $message, $comment, $this->getUser(), $this->messageLocalizer ); |
424 | } elseif ( $action === 'fuzzy' && $code === 'en' ) { |
425 | return self::doFuzzy( $title, $message, $comment, $this->getUser(), $this->messageLocalizer ); |
426 | } else { |
427 | throw new InvalidArgumentException( "Unhandled action $action" ); |
428 | } |
429 | } |
430 | |
431 | protected function checkProcessTime() { |
432 | return (int)wfTimestamp() - $this->time >= self::MAX_PROCESSING_TIME; |
433 | } |
434 | |
435 | /** @return string[] */ |
436 | private static function doImport( |
437 | Title $title, |
438 | string $message, |
439 | string $summary, |
440 | User $user, |
441 | MessageLocalizer $messageLocalizer |
442 | ): array { |
443 | $mwServices = MediaWikiServices::getInstance(); |
444 | $wikiPage = $mwServices->getWikiPageFactory()->newFromTitle( $title ); |
445 | $content = ContentHandler::makeContent( $message, $title ); |
446 | |
447 | $updater = $wikiPage->newPageUpdater( $user )->setContent( SlotRecord::MAIN, $content ); |
448 | if ( $user->authorizeWrite( 'autopatrol', $title ) ) { |
449 | $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED ); |
450 | } |
451 | $updater->saveRevision( CommentStoreComment::newUnsavedComment( $summary ) ); |
452 | $status = $updater->getStatus(); |
453 | $success = $status->isOK(); |
454 | |
455 | if ( $success ) { |
456 | return [ 'translate-manage-import-ok', |
457 | wfEscapeWikiText( $title->getPrefixedText() ) |
458 | ]; |
459 | } |
460 | |
461 | $statusFormatter = $mwServices |
462 | ->getFormatterFactory() |
463 | ->getStatusFormatter( $messageLocalizer ); |
464 | $text = "Failed to import new version of page {$title->getPrefixedText()}\n"; |
465 | $text .= $statusFormatter->getWikiText( $status ); |
466 | throw new RuntimeException( $text ); |
467 | } |
468 | |
469 | /** @return string[] */ |
470 | public static function doFuzzy( |
471 | Title $title, |
472 | string $message, |
473 | string $comment, |
474 | ?User $user, |
475 | MessageLocalizer $messageLocalizer |
476 | ): array { |
477 | $context = RequestContext::getMain(); |
478 | $services = MediaWikiServices::getInstance(); |
479 | |
480 | if ( !$context->getUser()->isAllowed( 'translate-manage' ) ) { |
481 | return [ 'badaccess-group0' ]; |
482 | } |
483 | |
484 | // Edit with fuzzybot if there is no user. |
485 | if ( !$user ) { |
486 | $user = FuzzyBot::getUser(); |
487 | } |
488 | |
489 | // Work on all subpages of base title. |
490 | $handle = new MessageHandle( $title ); |
491 | $titleText = $handle->getKey(); |
492 | |
493 | $revStore = $services->getRevisionStore(); |
494 | $dbw = $services->getDBLoadBalancer()->getConnection( DB_PRIMARY ); |
495 | $rows = $revStore->newSelectQueryBuilder( $dbw ) |
496 | ->joinPage() |
497 | ->where( [ |
498 | 'page_namespace' => $title->getNamespace(), |
499 | 'page_latest=rev_id', |
500 | 'page_title' . $dbw->buildLike( "$titleText/", $dbw->anyString() ), |
501 | ] ) |
502 | ->caller( __METHOD__ ) |
503 | ->fetchResultSet(); |
504 | |
505 | $changed = []; |
506 | $slots = $revStore->getContentBlobsForBatch( $rows, [ SlotRecord::MAIN ] )->getValue(); |
507 | |
508 | foreach ( $rows as $row ) { |
509 | global $wgTranslateDocumentationLanguageCode; |
510 | |
511 | $translationTitle = Title::makeTitle( (int)$row->page_namespace, $row->page_title ); |
512 | |
513 | // No fuzzy for English original or documentation language code. |
514 | if ( $translationTitle->getSubpageText() === 'en' || |
515 | $translationTitle->getSubpageText() === $wgTranslateDocumentationLanguageCode |
516 | ) { |
517 | // Use imported text, not database text. |
518 | $text = $message; |
519 | } elseif ( isset( $slots[$row->rev_id] ) ) { |
520 | $slot = $slots[$row->rev_id][SlotRecord::MAIN]; |
521 | $text = MessageHandle::makeFuzzyString( $slot->blob_data ); |
522 | } else { |
523 | $text = MessageHandle::makeFuzzyString( |
524 | Utilities::getTextFromTextContent( |
525 | $revStore->newRevisionFromRow( $row )->getContent( SlotRecord::MAIN ) |
526 | ) |
527 | ); |
528 | } |
529 | |
530 | // Do actual import |
531 | $changed[] = self::doImport( |
532 | $translationTitle, |
533 | $text, |
534 | $comment, |
535 | $user, |
536 | $messageLocalizer |
537 | ); |
538 | } |
539 | |
540 | // Format return text |
541 | $text = ''; |
542 | foreach ( $changed as $c ) { |
543 | $key = array_shift( $c ); |
544 | $text .= '* ' . $context->msg( $key, $c )->plain() . "\n"; |
545 | } |
546 | |
547 | return [ 'translate-manage-import-fuzzy', "\n" . $text ]; |
548 | } |
549 | |
550 | /** |
551 | * Given a group, message key and language code, creates a title for the |
552 | * translation page. |
553 | * |
554 | * @param MessageGroup $group |
555 | * @param string $key Message key |
556 | * @param string $code Language code |
557 | * @return Title |
558 | */ |
559 | private function makeTranslationTitle( MessageGroup $group, string $key, string $code ): Title { |
560 | $ns = $group->getNamespace(); |
561 | |
562 | return Title::makeTitleSafe( $ns, "$key/$code" ); |
563 | } |
564 | |
565 | /** |
566 | * Make section elements. |
567 | * |
568 | * @param string $legend Legend as raw html. |
569 | * @param string $type Contents of type class. |
570 | * @param string $content Contents as raw html. |
571 | * @param Language|null $lang The language in which the text is written. |
572 | * @return string Section element as html. |
573 | */ |
574 | public static function makeSectionElement( |
575 | string $legend, |
576 | string $type, |
577 | string $content, |
578 | ?Language $lang = null |
579 | ): string { |
580 | $containerParams = [ 'class' => "mw-tpt-sp-section mw-tpt-sp-section-type-{$type}" ]; |
581 | $legendParams = [ 'class' => 'mw-tpt-sp-legend' ]; |
582 | $contentParams = [ 'class' => 'mw-tpt-sp-content' ]; |
583 | if ( $lang ) { |
584 | $contentParams['dir'] = $lang->getDir(); |
585 | $contentParams['lang'] = $lang->getCode(); |
586 | } |
587 | |
588 | return Html::rawElement( 'div', $containerParams, |
589 | Html::rawElement( 'div', $legendParams, $legend ) . |
590 | Html::rawElement( 'div', $contentParams, $content ) |
591 | ); |
592 | } |
593 | |
594 | /** |
595 | * Escape name such that it validates as name and id parameter in html, and |
596 | * so that we can get it back with WebRequest::getVal(). Especially dot and |
597 | * spaces are difficult for the latter. |
598 | */ |
599 | private static function escapeNameForPHP( string $name ): string { |
600 | $replacements = [ |
601 | '(' => '(OP)', |
602 | ' ' => '(SP)', |
603 | "\t" => '(TAB)', |
604 | '.' => '(DOT)', |
605 | "'" => '(SQ)', |
606 | "\"" => '(DQ)', |
607 | '%' => '(PC)', |
608 | '&' => '(AMP)', |
609 | ]; |
610 | |
611 | return strtr( $name, $replacements ); |
612 | } |
613 | } |