summaryrefslogtreecommitdiff
blob: 678f50426bae6e0b03c297dfca3d57229ca55a0f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<?php
declare( strict_types = 1 );

namespace MediaWiki\Extension\Translate\Validation\Validators;

use Language;
use MediaWiki\Extension\Translate\Validation\MessageValidator;
use MediaWiki\Extension\Translate\Validation\ValidationIssue;
use MediaWiki\Extension\Translate\Validation\ValidationIssues;
use MediaWiki\MediaWikiServices;
use Parser;
use ParserOptions;
use PPFrame;
use TMessage;
use User;

/**
 * Handles plural validation for MediaWiki inline plural syntax.
 * @author Abijeet Patro
 * @license GPL-2.0-or-later
 * @since 2019.06
 */
class MediaWikiPluralValidator implements MessageValidator {
	public function getIssues( TMessage $message, string $targetLanguage ): ValidationIssues {
		$issues = new ValidationIssues();
		$this->pluralCheck( $message, $issues );
		$this->pluralFormsCheck( $message, $targetLanguage, $issues );

		return $issues;
	}

	private function pluralCheck( TMessage $message, ValidationIssues $issues ): void {
		$definition = $message->definition();
		$translation = $message->translation();

		if (
			stripos( $definition, '{{plural:' ) !== false &&
			stripos( $translation, '{{plural:' ) === false
		) {
			$issue = new ValidationIssue( 'plural', 'missing', 'translate-checks-plural' );
			$issues->add( $issue );
		}
	}

	protected function pluralFormsCheck(
		TMessage $message, string $code, ValidationIssues $issues
	): void {
		$translation = $message->translation();
		// Are there any plural forms for this language in this message?
		if ( stripos( $translation, '{{plural:' ) === false ) {
			return;
		}

		$plurals = self::getPluralForms( $translation );
		$allowed = self::getPluralFormCount( $code );

		foreach ( $plurals as $forms ) {
			$forms = self::removeExplicitPluralForms( $forms );
			$provided = count( $forms );

			if ( $provided > $allowed ) {
				$issue = new ValidationIssue(
					'plural',
					'forms',
					'translate-checks-plural-forms',
					[
						[ 'COUNT', $provided ],
						[ 'COUNT', $allowed ],
					]
				);

				$issues->add( $issue );
			}

			// Are the last two forms identical?
			if ( $provided > 1 && $forms[$provided - 1] === $forms[$provided - 2] ) {
				$issue = new ValidationIssue( 'plural',	'dupe', 'translate-checks-plural-dupe' );
				$issues->add( $issue );
			}
		}
	}

	/** Returns the number of plural forms %MediaWiki supports for a language. */
	public static function getPluralFormCount( string $code ): int {
		$forms = Language::factory( $code )->getPluralRules();

		// +1 for the 'other' form
		return count( $forms ) + 1;
	}

	/**
	 * Ugly home made probably awfully slow looping parser that parses {{PLURAL}} instances from
	 * a message and returns array of invocations having array of forms.
	 *
	 * @return array[]
	 */
	public static function getPluralForms( string $translation ): array {
		// Stores the forms from plural invocations
		$plurals = [];

		$cb = static function ( $parser, $frame, $args ) use ( &$plurals ) {
			$forms = [];

			foreach ( $args as $index => $form ) {
				// The first arg is the number, we skip it
				if ( $index !== 0 ) {
					// Collect the raw text
					$forms[] = $frame->expand( $form, PPFrame::RECOVER_ORIG );
					// Expand the text to process embedded plurals
					$frame->expand( $form );
				}
			}
			$plurals[] = $forms;

			return '';
		};

		// Setup parser
		$parser = MediaWikiServices::getInstance()->getParserFactory()->create();
		// Load the default magic words etc now.
		$parser->firstCallInit();
		// So that they don't overrider our own callback
		$parser->setFunctionHook( 'plural', $cb, Parser::SFH_NO_HASH | Parser::SFH_OBJECT_ARGS );

		// Setup things needed for preprocess
		$title = null;
		$options = new ParserOptions( new User(), Language::factory( 'en' ) );

		$parser->preprocess( $translation, $title, $options );

		return $plurals;
	}

	/** Remove forms that start with an explicit number. */
	public static function removeExplicitPluralForms( array $forms ): array {
		// Handle explicit 0= and 1= forms
		foreach ( $forms as $index => $form ) {
			if ( preg_match( '/^[0-9]+=/', $form ) ) {
				unset( $forms[$index] );
			}
		}

		return array_values( $forms );
	}
}