Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
46.67% |
28 / 60 |
|
57.14% |
4 / 7 |
CRAP | |
0.00% |
0 / 1 |
MediaWikiPluralValidator | |
46.67% |
28 / 60 |
|
57.14% |
4 / 7 |
67.15 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getIssues | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
pluralCheck | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
pluralFormsCheck | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
42 | |||
getPluralFormCount | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getPluralForms | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
3 | |||
removeExplicitPluralForms | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\Validation\Validators; |
5 | |
6 | use MediaWiki\Extension\Translate\MessageLoading\Message; |
7 | use MediaWiki\Extension\Translate\Validation\MessageValidator; |
8 | use MediaWiki\Extension\Translate\Validation\ValidationIssue; |
9 | use MediaWiki\Extension\Translate\Validation\ValidationIssues; |
10 | use MediaWiki\Languages\LanguageFactory; |
11 | use MediaWiki\User\UserFactory; |
12 | use Parser; |
13 | use ParserFactory; |
14 | use ParserOptions; |
15 | use 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 | */ |
23 | class 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 | } |