Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.67% covered (danger)
46.67%
28 / 60
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MediaWikiPluralValidator
46.67% covered (danger)
46.67%
28 / 60
57.14% covered (warning)
57.14%
4 / 7
67.15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getIssues
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 pluralCheck
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 pluralFormsCheck
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
 getPluralFormCount
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPluralForms
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 removeExplicitPluralForms
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Validation\Validators;
5
6use MediaWiki\Extension\Translate\MessageLoading\Message;
7use MediaWiki\Extension\Translate\Validation\MessageValidator;
8use MediaWiki\Extension\Translate\Validation\ValidationIssue;
9use MediaWiki\Extension\Translate\Validation\ValidationIssues;
10use MediaWiki\Languages\LanguageFactory;
11use MediaWiki\User\UserFactory;
12use Parser;
13use ParserFactory;
14use ParserOptions;
15use PPFrame;
16
17/**
18 * Handles plural validation for MediaWiki inline plural syntax.
19 * @author Abijeet Patro
20 * @license GPL-2.0-or-later
21 * @since 2019.06
22 */
23class MediaWikiPluralValidator implements MessageValidator {
24    /** @var LanguageFactory */
25    private $languageFactory;
26    /** @var ParserFactory */
27    private $parserFactory;
28    /** @var UserFactory */
29    private $userFactory;
30
31    public function __construct(
32        LanguageFactory $languageFactory,
33        ParserFactory $parserFactory,
34        UserFactory $userFactory
35    ) {
36        $this->languageFactory = $languageFactory;
37        $this->parserFactory = $parserFactory;
38        $this->userFactory = $userFactory;
39    }
40
41    public function getIssues( Message $message, string $targetLanguage ): ValidationIssues {
42        $issues = new ValidationIssues();
43        $this->pluralCheck( $message, $issues );
44        $this->pluralFormsCheck( $message, $targetLanguage, $issues );
45
46        return $issues;
47    }
48
49    private function pluralCheck( Message $message, ValidationIssues $issues ): void {
50        $definition = $message->definition();
51        $translation = $message->translation();
52
53        if (
54            stripos( $definition, '{{plural:' ) !== false &&
55            stripos( $translation, '{{plural:' ) === false
56        ) {
57            $issue = new ValidationIssue( 'plural', 'missing', 'translate-checks-plural' );
58            $issues->add( $issue );
59        }
60    }
61
62    protected function pluralFormsCheck(
63        Message $message, string $code, ValidationIssues $issues
64    ): void {
65        $translation = $message->translation();
66        // Are there any plural forms for this language in this message?
67        if ( stripos( $translation, '{{plural:' ) === false ) {
68            return;
69        }
70
71        $plurals = $this->getPluralForms( $translation );
72        $allowed = $this->getPluralFormCount( $code );
73
74        foreach ( $plurals as $forms ) {
75            $forms = self::removeExplicitPluralForms( $forms );
76            $provided = count( $forms );
77
78            if ( $provided > $allowed ) {
79                $issue = new ValidationIssue(
80                    'plural',
81                    'forms',
82                    'translate-checks-plural-forms',
83                    [
84                        [ 'COUNT', $provided ],
85                        [ 'COUNT', $allowed ],
86                    ]
87                );
88
89                $issues->add( $issue );
90            }
91
92            // Are the last two forms identical?
93            if ( $provided > 1 && $forms[$provided - 1] === $forms[$provided - 2] ) {
94                $issue = new ValidationIssue( 'plural',    'dupe', 'translate-checks-plural-dupe' );
95                $issues->add( $issue );
96            }
97        }
98    }
99
100    /** Returns the number of plural forms %MediaWiki supports for a language. */
101    public function getPluralFormCount( string $code ): int {
102        $forms = $this->languageFactory->getLanguage( $code )->getPluralRules();
103
104        // +1 for the 'other' form
105        return count( $forms ) + 1;
106    }
107
108    /**
109     * Ugly home made probably awfully slow looping parser that parses {{PLURAL}} instances from
110     * a message and returns array of invocations having array of forms.
111     *
112     * @return array[]
113     */
114    public function getPluralForms( string $translation ): array {
115        // Stores the forms from plural invocations
116        $plurals = [];
117
118        $cb = static function ( $parser, $frame, $args ) use ( &$plurals ) {
119            $forms = [];
120
121            foreach ( $args as $index => $form ) {
122                // The first arg is the number, we skip it
123                if ( $index !== 0 ) {
124                    // Collect the raw text
125                    $forms[] = $frame->expand( $form, PPFrame::RECOVER_ORIG );
126                    // Expand the text to process embedded plurals
127                    $frame->expand( $form );
128                }
129            }
130            $plurals[] = $forms;
131
132            return '';
133        };
134
135        // Setup parser
136        $parser = $this->parserFactory->create();
137        $parser->setFunctionHook( 'plural', $cb, Parser::SFH_NO_HASH | Parser::SFH_OBJECT_ARGS );
138
139        // Setup things needed for preprocess
140        $title = null;
141        $options = ParserOptions::newFromUserAndLang(
142            $this->userFactory->newAnonymous(),
143            $this->languageFactory->getLanguage( 'en' )
144        );
145
146        $parser->preprocess( $translation, $title, $options );
147
148        return $plurals;
149    }
150
151    /** Remove forms that start with an explicit number. */
152    public static function removeExplicitPluralForms( array $forms ): array {
153        // Handle explicit 0= and 1= forms
154        foreach ( $forms as $index => $form ) {
155            if ( preg_match( '/^[0-9]+=/', $form ) ) {
156                unset( $forms[$index] );
157            }
158        }
159
160        return array_values( $forms );
161    }
162}