wikimedia/mediawiki-extensions-Translate

View on GitHub
src/Statistics/TranslationStatsSpecialPage.php

Summary

Maintainability
C
1 day
Test Coverage
<?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() )
            )
        );
    }
}