Localization fix.
[mediawiki.git] / maintenance / language / checkLanguage.inc
blob2cfd1b049befcb5e9ff82d948121ba593a4a8498
1 <?php
2 /**
3  * @ingroup MaintenanceLanguage
4  */
6 class CheckLanguageCLI {
7         protected $code  = null;
8         protected $level = 2;
9         protected $doLinks = false;
10         protected $wikiCode = 'en';
11         protected $checkAll = false;
12         protected $output = 'plain';
13         protected $checks = array();
14         protected $L = null;
16         protected $defaultChecks = array(
17                 'untranslated', 'obsolete', 'variables', 'empty', 'plural',
18                 'whitespace', 'xhtml', 'chars', 'links', 'unbalanced'
19         );
21         protected $results = array();
23         private $includeExif = false;
25         /**
26          * GLOBALS: $wgLanguageCode;
27          */
28         public function __construct( Array $options ) {
30                 if ( isset( $options['help'] ) ) {
31                         echo $this->help();
32                         exit();
33                 }
35                 if ( isset($options['lang']) ) {
36                         $this->code = $options['lang'];
37                 } else {
38                         global $wgLanguageCode;
39                         $this->code = $wgLanguageCode;
40                 }
42                 if ( isset($options['level']) ) {
43                         $this->level = $options['level'];
44                 }
46                 $this->doLinks = isset($options['links']);
47                 $this->includeExif = !isset($options['noexif']);
48                 $this->checkAll = isset($options['all']);
50                 if ( isset($options['wikilang']) ) {
51                         $this->wikiCode = $options['wikilang'];
52                 }
54                 if ( isset( $options['whitelist'] ) ) {
55                         $this->checks = explode( ',', $options['whitelist'] );
56                 } elseif ( isset( $options['blacklist'] ) ) {
57                         $this->checks = array_diff(
58                                 $this->defaultChecks,
59                                 explode( ',', $options['blacklist'] )
60                         );
61                 } else {
62                         $this->checks = $this->defaultChecks;
63                 }
65                 if ( isset($options['output']) ) {
66                         $this->output = $options['output'];
67                 }
69                 # Some additional checks not enabled by default
70                 if ( isset( $options['duplicate'] ) ) {
71                         $this->checks[] = 'duplicate';
72                 }
74                 $this->L = new languages( $this->includeExif );
75         }
77         protected function getChecks() {
78                 $checks = array();
79                 $checks['untranslated'] = 'getUntranslatedMessages';
80                 $checks['duplicate'] = 'getDuplicateMessages';
81                 $checks['obsolete'] = 'getObsoleteMessages';
82                 $checks['variables'] = 'getMessagesWithoutVariables';
83                 $checks['plural'] = 'getMessagesWithoutPlural';
84                 $checks['empty'] = 'getEmptyMessages';
85                 $checks['whitespace'] = 'getMessagesWithWhitespace';
86                 $checks['xhtml'] = 'getNonXHTMLMessages';
87                 $checks['chars'] = 'getMessagesWithWrongChars';
88                 $checks['links'] = 'getMessagesWithDubiousLinks';
89                 $checks['unbalanced'] = 'getMessagesWithUnbalanced';
90                 return $checks;
91         }
93         protected function getDescriptions() {
94                 $descriptions = array();
95                 $descriptions['untranslated'] = '$1 message(s) of $2 are not translated to $3, but exist in en:';
96                 $descriptions['duplicate'] = '$1 message(s) of $2 are translated the same in en and $3:';
97                 $descriptions['obsolete'] = '$1 message(s) of $2 do not exist in en or are in the ignore list, but are in $3';
98                 $descriptions['variables'] = '$1 message(s) of $2 in $3 don\'t use some variables that en uses:';
99                 $descriptions['plural'] = '$1 message(s) of $2 in $3 don\'t use {{plural}} while en uses:';
100                 $descriptions['empty'] = '$1 message(s) of $2 in $3 are empty or -:';
101                 $descriptions['whitespace'] = '$1 message(s) of $2 in $3 have trailing whitespace:';
102                 $descriptions['xhtml'] = '$1 message(s) of $2 in $3 contain illegal XHTML:';
103                 $descriptions['chars'] = '$1 message(s) of $2 in $3 include hidden chars which should not be used in the messages:';
104                 $descriptions['links'] = '$1 message(s) of $2 in $3 have problematic link(s):';
105                 $descriptions['unbalanced'] = '$1 message(s) of $2 in $3 have unbalanced {[]}:';
106                 return $descriptions;
107         }
109         protected function help() {
110                 return <<<ENDS
111 Run this script to check a specific language file, or all of them.
112 Command line settings are in form --parameter[=value].
113 Parameters:
114         * lang: Language code (default: the installation default language).
115         * all: Check all customized languages.
116         * help: Show this help.
117         * level: Show the following level (default: 2).
118         * links: Link the message values (default off).
119         * wikilang: For the links, what is the content language of the wiki to display the output in (default en).
120         * whitelist: Do only the following checks (form: code,code).
121         * blacklist: Don't do the following checks (form: code,code).
122         * duplicate: Additionally check for messages which are translated the same to English (default off).
123         * noexif: Don't check for EXIF messages (a bit hard and boring to translate), if you know that they are currently not translated and want to focus on other problems (default off).
124 Check codes (ideally, all of them should result 0; all the checks are executed by default (except duplicate and language specific check blacklists in checkLanguage.inc):
125         * untranslated: Messages which are required to translate, but are not translated.
126         * duplicate: Messages which translation equal to fallback
127         * obsolete: Messages which are untranslatable, but translated.
128         * variables: Messages without variables which should be used.
129         * empty: Empty messages.
130         * whitespace: Messages which have trailing whitespace.
131         * xhtml: Messages which are not well-formed XHTML (checks only few common errors).
132         * chars: Messages with hidden characters.
133         * links: Messages which contains broken links to pages (does not find all).
134         * unbalanced: Messages which contains unequal numbers of opening {[ and closing ]}.
135 Display levels (default: 2):
136         * 0: Skip the checks (useful for checking syntax).
137         * 1: Show only the stub headers and number of wrong messages, without list of messages.
138         * 2: Show only the headers and the message keys, without the message values.
139         * 3: Show both the headers and the complete messages, with both keys and values.
141 ENDS;
142         }
144         public function execute() {
145                 $this->doChecks();
146                 if ( $this->level > 0 ) {
147                         switch ($this->output) {
148                                 case 'plain':
149                                         $this->outputText();
150                                         break;
151                                 case 'wiki':
152                                         $this->outputWiki();
153                                         break;
154                                 default:
155                                         throw new MWException( "Invalid output type $this->output");
156                         }
157                 }
158         }
160         protected function doChecks() {
161                 $ignoredCodes = array( 'en', 'enRTL' );
163                 $this->results = array();
164                 # Check the language
165                 if ( $this->checkAll ) {
166                         foreach ( $this->L->getLanguages() as $language ) {
167                                 if ( !in_array($language, $ignoredCodes) ) {
168                                         $this->results[$language] = $this->checkLanguage( $language );
169                                 }
170                         }
171                 } else {
172                         if ( in_array($this->code, $ignoredCodes) ) {
173                                 throw new MWException("Cannot check code $this->code.");
174                         } else {
175                                 $this->results[$this->code] = $this->checkLanguage( $this->code );
176                         }
177                 }
178         }
180         protected function getCheckBlacklist() {
181                 global $checkBlacklist;
182                 return $checkBlacklist;
183         }
185         protected function checkLanguage( $code ) {
186                 # Syntax check only
187                 if ( $this->level === 0 ) {
188                         $this->L->getMessages( $code );
189                         return;
190                 }
192                 $results = array();
193                 $checkFunctions = $this->getChecks();
194                 $checkBlacklist = $this->getCheckBlacklist();
195                 foreach ( $this->checks as $check ) {
196                         if ( isset($checkBlacklist[$code]) &&
197                                 in_array($check, $checkBlacklist[$code]) ) {
198                                 $result[$check] = array();
199                                 continue;
200                         }
202                         $callback = array( $this->L, $checkFunctions[$check] );
203                         if ( !is_callable($callback ) ) {
204                                 throw new MWException( "Unkown check $check." );
205                         }
206                         $results[$check] = call_user_func( $callback , $code );
207                 }
209                 return $results;
210         }
212         protected function formatKey( $key, $code ) {
213                 if ( $this->doLinks ) {
214                         $displayKey = ucfirst( $key );
215                         if ( $code == $this->wikiCode ) {
216                                 return "[[MediaWiki:$displayKey|$key]]";
217                         } else {
218                                 return "[[MediaWiki:$displayKey/$code|$key]]";
219                         }
220                 } else {
221                         return $key;
222                 }
223         }
225         protected function outputText() {
226                 foreach ( $this->results as $code => $results ) {
227                         $translated = $this->L->getMessages( $code );
228                         $translated = count( $translated['translated'] );
229                         $translatable = $this->L->getGeneralMessages();
230                         $translatable = count( $translatable['translatable'] );
231                         foreach ( $results as $check => $messages ) {
232                                 $count = count( $messages );
233                                 if ( $count ) {
234                                         $search = array( '$1', '$2', '$3' );
235                                         $replace = array( $count, $check == 'untranslated' ? $translatable: $translated, $code );
236                                         $descriptions = $this->getDescriptions();
237                                         echo "\n" . str_replace( $search, $replace, $descriptions[$check] ) . "\n";
238                                         if ( $this->level == 1 ) {
239                                                 echo "[messages are hidden]\n";
240                                         } else {
241                                                 foreach ( $messages as $key => $value ) {
242                                                         $displayKey = $this->formatKey( $key, $code );
243                                                         if ( $this->level == 2 ) {
244                                                                 echo "* $displayKey\n";
245                                                         } else {
246                                                                 echo "* $displayKey:            '$value'\n";
247                                                         }
248                                                 }
249                                         }
250                                 }
251                         }
252                 }
253         }
255         /**
256          * Globals: $wgContLang, $IP
257          */
258         function outputWiki() {
259                 global $wgContLang, $IP;
260                 $detailText = '';
261                 $rows[] = '! Language !! Code !! Total !! ' . implode( ' !! ', $this->checks );
262                 foreach ( $this->results as $code => $results ) {
263                         $detailTextForLang = "==$code==\n";
264                         $numbers = array();
265                         $problems = 0;
266                         $detailTextForLangChecks = array();
267                         foreach ( $results as $check => $messages ) {
268                                 $count = count( $messages );
269                                 if ( $count ) {
270                                         $problems += $count;
271                                         $messageDetails = array();
272                                         foreach ( $messages as $key => $details ) {
273                                                 $displayKey = $this->formatKey( $key, $code );
274                                                 $messageDetails[] = $displayKey;
275                                         }
276                                         $detailTextForLangChecks[] = "===$code-$check===\n* " . implode( ', ', $messageDetails );
277                                         $numbers[] = "'''[[#$code-$check|$count]]'''";
278                                 } else {
279                                         $numbers[] = $count;
280                                 }
282                         }
284                         if ( count( $detailTextForLangChecks ) ) {
285                                 $detailText .= $detailTextForLang . implode( "\n", $detailTextForLangChecks ) . "\n";
286                         }
288                         if ( !$problems ) { continue; } // Don't list languages without problems
289                         $language = $wgContLang->getLanguageName( $code );
290                         $rows[] = "| $language || $code || $problems || " . implode( ' || ', $numbers );
291                 }
293                 $tableRows = implode( "\n|-\n", $rows );
295                 $version = SpecialVersion::getVersion( $IP );
296                 echo <<<EOL
297 '''Check results are for:''' <code>$version</code>
300 {| class="sortable wikitable" border="2" cellpadding="4" cellspacing="0" style="background-color: #F9F9F9; border: 1px #AAAAAA solid; border-collapse: collapse; clear:both;"
301 $tableRows
304 $detailText
306 EOL;
307         }
309         protected function isEmpty() {
310                 $empty = true;
311                 foreach( $this->results as $code => $results ) {
312                         foreach( $results as $check => $messages ) {
313                                 if( !empty( $messages ) ) {
314                                         $empty = false;
315                                         break;
316                                 }
317                         }
318                         if( !$empty ) {
319                                 break;
320                         }
321                 }
322                 return $empty;
323         }
326 class CheckExtensionsCLI extends CheckLanguageCLI {
327         private $extensions;
329         public function __construct( Array $options, $extension ) {
330                 if ( isset( $options['help'] ) ) {
331                         echo $this->help();
332                         exit();
333                 }
335                 if ( isset($options['lang']) ) {
336                         $this->code = $options['lang'];
337                 } else {
338                         global $wgLanguageCode;
339                         $this->code = $wgLanguageCode;
340                 }
342                 if ( isset($options['level']) ) {
343                         $this->level = $options['level'];
344                 }
346                 $this->doLinks = isset($options['links']);
348                 if ( isset($options['wikilang']) ) {
349                         $this->wikiCode = $options['wikilang'];
350                 }
352                 if ( isset( $options['whitelist'] ) ) {
353                         $this->checks = explode( ',', $options['whitelist'] );
354                 } elseif ( isset( $options['blacklist'] ) ) {
355                         $this->checks = array_diff(
356                                 $this->defaultChecks,
357                                 explode( ',', $options['blacklist'] )
358                         );
359                 } else {
360                         $this->checks = $this->defaultChecks;
361                 }
363                 if ( isset($options['output']) ) {
364                         $this->output = $options['output'];
365                 }
367                 # Some additional checks not enabled by default
368                 if ( isset( $options['duplicate'] ) ) {
369                         $this->checks[] = 'duplicate';
370                 }
372                 $this->extensions = array();
373                 $extensions = new PremadeMediawikiExtensionGroups();
374                 $extensions->addAll();
375                 if( $extension == 'all' ) {
376                         foreach( MessageGroups::singleton()->getGroups() as $group ) {
377                                 if( strpos( $group->getId(), 'ext-' ) === 0 && !$group->isMeta() ) {
378                                         $this->extensions[] = new extensionLanguages( $group );
379                                 }
380                         }
381                 } elseif( $extension == 'wikimedia' ) {
382                         $wikimedia = MessageGroups::getGroup( 'ext-0-wikimedia' );
383                         foreach( $wikimedia->wmfextensions() as $extension ) {
384                                 $group = MessageGroups::getGroup( $extension );
385                                 $this->extensions[] = new extensionLanguages( $group );
386                         }
387                 } else {
388                         $extensions = explode( ',', $extension );
389                         foreach( $extensions as $extension ) {
390                                 $group = MessageGroups::getGroup( 'ext-' . $extension );
391                                 if( $group ) {
392                                         $extension = new extensionLanguages( $group );
393                                         $this->extensions[] = $extension;
394                                 } else {
395                                         print "No such extension $extension.\n";
396                                 }
397                         }
398                 }
399         }
401         protected function help() {
402                 return <<<ENDS
403 Run this script to check the status of a specific language in extensions, or all of them.
404 Command line settings are in form --parameter[=value], except for the first one.
405 Parameters:
406         * First parameter (mandatory): Extension name, multiple extension names (separated by commas), "all" for all the extensions or "wikimedia" for extensions used by Wikimedia.
407         * lang: Language code (default: the installation default language).
408         * help: Show this help.
409         * level: Show the following level (default: 2).
410         * links: Link the message values (default off).
411         * wikilang: For the links, what is the content language of the wiki to display the output in (default en).
412         * whitelist: Do only the following checks (form: code,code).
413         * blacklist: Do not perform the following checks (form: code,code).
414         * duplicate: Additionally check for messages which are translated the same to English (default off).
415 Check codes (ideally, all of them should result 0; all the checks are executed by default (except duplicate and language specific check blacklists in checkLanguage.inc):
416         * untranslated: Messages which are required to translate, but are not translated.
417         * duplicate: Messages which translation equal to fallback
418         * obsolete: Messages which are untranslatable, but translated.
419         * variables: Messages without variables which should be used.
420         * empty: Empty messages.
421         * whitespace: Messages which have trailing whitespace.
422         * xhtml: Messages which are not well-formed XHTML (checks only few common errors).
423         * chars: Messages with hidden characters.
424         * links: Messages which contains broken links to pages (does not find all).
425         * unbalanced: Messages which contains unequal numbers of opening {[ and closing ]}.
426 Display levels (default: 2):
427         * 0: Skip the checks (useful for checking syntax).
428         * 1: Show only the stub headers and number of wrong messages, without list of messages.
429         * 2: Show only the headers and the message keys, without the message values.
430         * 3: Show both the headers and the complete messages, with both keys and values.
432 ENDS;
433         }
435         public function execute() {
436                 $this->doChecks();
437         }
439         protected function checkLanguage( $code ) {
440                 foreach( $this->extensions as $extension ) {
441                         $this->L = $extension;
442                         $this->results = array();
443                         $this->results[$code] = parent::checkLanguage( $code );
445                         if( !$this->isEmpty() ) {
446                                 echo $extension->name() . ":\n";
448                                 if( $this->level > 0 ) {
449                                         switch( $this->output ) {
450                                                 case 'plain':
451                                                         $this->outputText();
452                                                         break;
453                                                 case 'wiki':
454                                                         $this->outputWiki();
455                                                         break;
456                                                 default:
457                                                         throw new MWException( "Invalid output type $this->output" );
458                                         }
459                                 }
461                                 echo "\n";
462                         }
463                 }
464         }
467 # Blacklist some checks for some languages
468 $checkBlacklist = array(
469 #'code'        => array( 'check1', 'check2' ... )
470 'gan'          => array( 'plural' ),
471 'gn'           => array( 'plural' ),
472 'hak'          => array( 'plural' ),
473 'hu'           => array( 'plural' ),
474 'ja'           => array( 'plural' ), // Does not use plural
475 'ka'           => array( 'plural' ),
476 'kk-arab'      => array( 'plural' ),
477 'kk-cyrl'      => array( 'plural' ),
478 'kk-latn'      => array( 'plural' ),
479 'ko'           => array( 'plural' ),
480 'mn'           => array( 'plural' ),
481 'ms'           => array( 'plural' ),
482 'my'           => array( 'chars' ),  // Uses a lot zwnj
483 'sah'          => array( 'plural' ),
484 'sq'           => array( 'plural' ),
485 'tet'          => array( 'plural' ),
486 'th'           => array( 'plural' ),
487 'wuu'          => array( 'plural' ),
488 'xmf'          => array( 'plural' ),
489 'yue'          => array( 'plural' ),
490 'zh'           => array( 'plural' ),
491 'zh-classical' => array( 'plural' ),
492 'zh-cn'        => array( 'plural' ),
493 'zh-hans'      => array( 'plural' ),
494 'zh-hant'      => array( 'plural' ),
495 'zh-hk'        => array( 'plural' ),
496 'zh-sg'        => array( 'plural' ),
497 'zh-tw'        => array( 'plural' ),
498 'zh-yue'       => array( 'plural' ),