src/Statistics/TranslationStatsSpecialPage.php
<?php
declare( strict_types = 1 );
namespace MediaWiki\Extension\Translate\Statistics;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
use MediaWiki\Extension\Translate\Utilities\JsSelectToInput;
use MediaWiki\Extension\Translate\Utilities\Utilities;
use MediaWiki\Html\FormOptions;
use MediaWiki\Html\Html;
use MediaWiki\SpecialPage\SpecialPage;
use Xml;
use XmlSelect;
use function wfEscapeWikiText;
/**
* Includable special page for generating graphs for statistics.
*
* @file
* @author Niklas Laxström
* @author Siebrand Mazeland
* @license GPL-2.0-or-later
*/
class TranslationStatsSpecialPage extends SpecialPage {
private TranslationStatsDataProvider $dataProvider;
private const GRAPH_CONTAINER_ID = 'translationStatsGraphContainer';
private const GRAPH_CONTAINER_CLASS = 'mw-translate-translationstats-container';
public function __construct( TranslationStatsDataProvider $dataProvider ) {
parent::__construct( 'TranslationStats' );
$this->dataProvider = $dataProvider;
}
/** @inheritDoc */
public function isIncludable(): bool {
return true;
}
/** @inheritDoc */
protected function getGroupName(): string {
return 'translation';
}
/** @inheritDoc */
public function execute( $par ): void {
$graphOpts = new TranslationStatsGraphOptions();
$graphOpts->bindArray( $this->getRequest()->getValues() );
$pars = explode( ';', (string)$par );
foreach ( $pars as $item ) {
if ( !str_contains( $item, '=' ) ) {
continue;
}
[ $key, $value ] = array_map( 'trim', explode( '=', $item, 2 ) );
if ( $graphOpts->hasValue( $key ) ) {
$graphOpts->setValue( $key, $value );
}
}
$graphOpts->normalize( $this->dataProvider->getGraphTypes() );
$opts = $graphOpts->getFormOptions();
if ( $this->including() ) {
$this->getOutput()->addHTML( $this->embed( $opts ) );
} else {
$this->form( $opts );
}
}
/**
* Constructs the form which can be used to generate custom graphs.
*
* @suppress SecurityCheck-DoubleEscaped Intentionally outputting what user should type
*/
private function form( FormOptions $opts ): void {
$script = $this->getConfig()->get( 'Script' );
$this->setHeaders();
$out = $this->getOutput();
$out->addModules( 'ext.translate.special.translationstats' );
$out->addHelpLink( 'Help:Extension:Translate/Statistics_and_reporting' );
$out->addWikiMsg( 'translate-statsf-intro' );
$out->addHTML(
Html::openElement( 'fieldset' ) .
Html::element( 'legend', null, $this->msg( 'translate-statsf-options' )->text() ) .
Html::openElement(
'form',
[ 'action' => $script, 'id' => 'translationStatsConfig' ]
) .
Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
Html::hidden( 'preview', 1 ) . '<table>'
);
$submit = Html::submitButton( $this->msg( 'translate-statsf-submit' )->text() );
$out->addHTML(
$this->eInput( 'width', $opts ) . $this->eInput( 'height', $opts ) .
'<tr><td colspan="2"><hr /></td></tr>' . $this->eInput( 'start', $opts, 24 ) .
$this->eInput( 'days', $opts ) .
$this->eRadio( 'scale', $opts, [ 'years', 'months', 'weeks', 'days', 'hours' ] ) .
$this->eRadio( 'count', $opts, $this->dataProvider->getGraphTypes() ) .
'<tr><td colspan="2"><hr /></td></tr>' . $this->eLanguage( 'language', $opts ) .
$this->eGroup( 'group', $opts ) . '<tr><td colspan="2"><hr /></td></tr>' .
'<tr><td colspan="2">' . $submit . '</td></tr>'
);
$out->addHTML( '</table></form></fieldset>' );
if ( !$opts['preview'] ) {
return;
}
$spiParams = [];
foreach ( $opts->getChangedValues() as $key => $v ) {
if ( $key === 'preview' ) {
continue;
}
if ( is_array( $v ) ) {
$v = implode( ',', $v );
if ( !strlen( $v ) ) {
continue;
}
}
$spiParams[] = $key . '=' . wfEscapeWikiText( $v );
}
$spiParams = $spiParams ? '/' . implode( ';', $spiParams ) : '';
$titleText = $this->getPageTitle()->getPrefixedText();
$out->addHTML( Html::element( 'hr' ) );
// Element to render the graph
$out->addHTML(
Html::rawElement(
'div',
[
'id' => self::GRAPH_CONTAINER_ID,
'style' => 'margin: 2em auto; display: block',
'class' => self::GRAPH_CONTAINER_CLASS,
]
)
);
$out->addHTML(
Html::element(
'pre',
[ 'aria-label' => $this->msg( 'translate-statsf-embed' )->text() ],
"{{{$titleText}{$spiParams}}}"
)
);
}
/// Construct HTML for a table row with label and input in two columns.
private function eInput( string $name, FormOptions $opts, int $width = 4 ): string {
$value = $opts[$name];
return '<tr><td>' . $this->eLabel( $name ) . '</td><td>' .
Html::input( $name, $value, 'text', [ 'id' => $name, 'size' => $width ] ) .
'</td></tr>' . "\n";
}
/// Construct HTML for a label for option.
private function eLabel( string $name ): string {
// Give grep a chance to find the usages:
// translate-statsf-width, translate-statsf-height, translate-statsf-start,
// translate-statsf-days, translate-statsf-scale, translate-statsf-count,
// translate-statsf-language, translate-statsf-group
$label = 'translate-statsf-' . $name;
$label = $this->msg( $label )->escaped();
return Xml::tags( 'label', [ 'for' => $name ], $label );
}
/// Construct HTML for a table row with label and radio input in two columns.
private function eRadio( string $name, FormOptions $opts, array $alts ): string {
// Give grep a chance to find the usages:
// translate-statsf-scale, translate-statsf-count
$label = 'translate-statsf-' . $name;
$label = $this->msg( $label )->escaped();
$s = '<tr><td>' . $label . '</td><td>';
$options = [];
foreach ( $alts as $alt ) {
$id = "$name-$alt";
$radio = Html::radio(
$name,
$alt === $opts[$name],
[
'value' => $alt,
'id' => $id
]
) . ' ';
$options[] = $radio . ' ' . $this->eLabel( $id );
}
$s .= implode( ' ', $options );
$s .= '</td></tr>' . "\n";
return $s;
}
/// Construct HTML for a table row with label and language selector in two columns.
private function eLanguage( string $name, FormOptions $opts ): string {
$value = implode( ',', $opts[$name] );
$select = $this->languageSelector();
$select->setTargetId( 'language' );
return '<tr><td>' . $this->eLabel( $name ) . '</td><td>' . $select->getHtmlAndPrepareJS() .
'<br />' . Html::input( $name, $value, 'text', [ 'id' => $name, 'size' => 20 ] ) .
'</td></tr>' . "\n";
}
/// Construct a JavaScript enhanced language selector.
private function languageSelector(): JsSelectToInput {
$languages = Utilities::getLanguageNames( $this->getLanguage()->getCode() );
ksort( $languages );
$selector = new XmlSelect( 'mw-language-selector', 'mw-language-selector' );
foreach ( $languages as $code => $name ) {
$selector->addOption( "$code - $name", $code );
}
return new JsSelectToInput( $selector );
}
/// Constructs HTML for a table row with label and group selector in two columns.
private function eGroup( string $name, FormOptions $opts ): string {
$value = implode( ',', $opts[$name] );
$select = $this->groupSelector();
$select->setTargetId( 'group' );
return '<tr><td>' . $this->eLabel( $name ) . '</td><td>' . $select->getHtmlAndPrepareJS() .
'<br />' . Html::input( $name, $value, 'text', [ 'id' => $name, 'size' => 20 ] ) .
'</td></tr>' . "\n";
}
/// Construct a JavaScript enhanced group selector.
private function groupSelector(): JsSelectToInput {
$groups = MessageGroups::singleton()->getGroups();
foreach ( $groups as $key => $group ) {
if ( !$group->exists() ) {
unset( $groups[$key] );
}
}
ksort( $groups );
$selector = new XmlSelect( 'mw-group-selector', 'mw-group-selector' );
foreach ( $groups as $code => $name ) {
$selector->addOption( $name->getLabel(), $code );
}
return new JsSelectToInput( $selector );
}
private function embed( FormOptions $opts ): string {
$this->getOutput()->addModules( 'ext.translate.translationstats.embedded' );
return Html::rawElement(
'div',
[
'class' => self::GRAPH_CONTAINER_CLASS,
],
Html::hidden(
'translationStatsGraphOptions',
json_encode( $opts->getAllValues() )
)
);
}
}