Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.18% covered (success)
98.18%
54 / 55
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
UnicodePluralValidator
98.18% covered (success)
98.18%
54 / 55
66.67% covered (warning)
66.67%
2 / 3
19
0.00% covered (danger)
0.00%
0 / 1
 getIssues
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
6
 pluralPresenceCheck
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
7.14
 pluralFormCheck
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Validation\Validators;
5
6use MediaWiki\Extension\Translate\MessageLoading\Message;
7use MediaWiki\Extension\Translate\Utilities\UnicodePlural;
8use MediaWiki\Extension\Translate\Validation\MessageValidator;
9use MediaWiki\Extension\Translate\Validation\ValidationIssue;
10use MediaWiki\Extension\Translate\Validation\ValidationIssues;
11
12/**
13 * This is a very strict validator class for Unicode CLDR based plural markup.
14 *
15 * It requires all forms to be present and in correct order. Whitespace around keywords
16 * and values is trimmed. The keyword `other` is left out, though it is allowed in input.
17 * @since 2019.09
18 * @license GPL-2.0-or-later
19 */
20class UnicodePluralValidator implements MessageValidator {
21    public function getIssues( Message $message, string $targetLanguage ): ValidationIssues {
22        $issues = new ValidationIssues();
23
24        // We skip certain validations if we don't know the expected keywords
25        $expectedKeywords = UnicodePlural::getPluralKeywords( $targetLanguage );
26
27        $definition = $message->definition();
28        $translation = $message->translation();
29        $definitionHasPlural = UnicodePlural::hasPlural( $definition );
30        $translationHasPlural = UnicodePlural::hasPlural( $translation );
31
32        $presence = $this->pluralPresenceCheck(
33            $definitionHasPlural,
34            $translationHasPlural
35        );
36
37        // Using same check keys as MediaWikiPluralValidator
38        if ( $presence === 'missing' && $expectedKeywords !== null ) {
39            $issue = new ValidationIssue( 'plural', 'missing', 'translate-checks-unicode-plural-missing' );
40            $issues->add( $issue );
41        } elseif ( $presence === 'unsupported' ) {
42            $issue = new ValidationIssue( 'plural', 'unsupported', 'translate-checks-unicode-plural-unsupported' );
43            $issues->add( $issue );
44        } elseif ( $presence === 'ok' ) {
45            [ $msgcode, $actualKeywords ] = $this->pluralFormCheck( $translation, $expectedKeywords );
46            if ( $msgcode === 'invalid' ) {
47                $formatter = fn ( string $x ) => [ $x, '…' ];
48                $expectedExample = UnicodePlural::flattenList(
49                    array_map( $formatter, $expectedKeywords ?? UnicodePlural::KEYWORDS )
50                );
51                $actualExample = UnicodePlural::flattenList(
52                    array_map( $formatter, $actualKeywords )
53                );
54
55                $issue = new ValidationIssue(
56                    'plural',
57                    'forms',
58                    'translate-checks-unicode-plural-invalid',
59                    [
60                        [ 'PLAIN', $expectedExample ],
61                        [ 'PLAIN', $actualExample ],
62                    ]
63                );
64                $issues->add( $issue );
65            }
66        }
67        // else: not-applicable
68
69        return $issues;
70    }
71
72    private function pluralPresenceCheck(
73        bool $definitionHasPlural,
74        bool $translationHasPlural
75    ): string {
76        if ( !$definitionHasPlural && $translationHasPlural ) {
77            return 'unsupported';
78        } elseif ( $definitionHasPlural && !$translationHasPlural ) {
79            return 'missing';
80        } elseif ( !$definitionHasPlural && !$translationHasPlural ) {
81            return 'not-applicable';
82        }
83
84        // Both have plural
85        return 'ok';
86    }
87
88    private function pluralFormCheck( string $text, ?array $expectedKeywords ): array {
89        [ , $instanceMap ] = UnicodePlural::parsePluralForms( $text );
90
91        foreach ( $instanceMap as $forms ) {
92            $actualKeywords = [];
93            foreach ( $forms as [ $keyword, ] ) {
94                $actualKeywords[] = $keyword;
95            }
96
97            if ( $expectedKeywords !== null ) {
98                if ( $actualKeywords !== $expectedKeywords ) {
99                    return [ 'invalid', $actualKeywords ];
100                }
101            } elseif ( array_diff( $actualKeywords, UnicodePlural::KEYWORDS ) !== [] ) {
102                // We don't know the actual forms for the language, but complain about those
103                // that are not valid in any language
104                return [ 'invalid', $actualKeywords ];
105            }
106        }
107
108        return [ 'ok', [] ];
109    }
110}