00001 <?php
00016 abstract class ComplexMessages {
00017 const LANG_MASTER = 0;
00018 const LANG_CHAIN = 1;
00019 const LANG_CURRENT = 2;
00020
00021 protected $language = null;
00022 protected $targetDir = null;
00023 protected $id = '__BUG__';
00024 protected $variable = '__BUG__';
00025 protected $data = array();
00026 protected $elementsInArray = true;
00027 protected $databaseMsg = '__BUG__';
00028 protected $chainable = false;
00029 protected $firstMagic = false;
00030 protected $constants = array();
00031
00032 protected $tableAttributes = array(
00033 'class' => 'wikitable',
00034 'border' => '2',
00035 'cellpadding' => '4',
00036 'cellspacing' => '0',
00037 'style' => 'background-color: #F9F9F9; border: 1px #AAAAAA solid; border-collapse: collapse;',
00038 );
00039
00040 public function __construct( $langCode ) {
00041 $this->language = $langCode;
00042
00043 $language = Language::factory( $langCode );
00044 $this->targetDir = $language->getDir();
00045 }
00046
00047 public function getTitle() {
00048
00049
00050 return wfMessage( 'translate-magic-' . $this->id )->text();
00051 }
00052
00053 #
00054 # Data retrieval
00055 #
00056 protected $init = false;
00057
00058 public function getGroups() {
00059 if ( !$this->init ) {
00060 $saved = $this->getSavedData();
00061 foreach ( $this->data as &$group ) {
00062 $this->getData( $group, $saved );
00063 }
00064 $this->init = true;
00065 }
00066
00067 return $this->data;
00068 }
00069
00070 public function cleanData( $defs, $current ) {
00071 foreach ( $current as $item => $values ) {
00072 if ( !$this->elementsInArray ) {
00073 break;
00074 }
00075
00076 if ( !isset( $defs[$item] ) ) {
00077 unset( $current[$item] );
00078 continue;
00079 }
00080
00081 foreach ( $values as $index => $value )
00082 if ( in_array( $value, $defs[$item], true ) ) {
00083 unset( $current[$item][$index] );
00084 }
00085 }
00086
00087 return $current;
00088 }
00089
00090 public function mergeMagic( $defs, $current ) {
00091 foreach ( $current as $item => &$values ) {
00092 $newchain = $defs[$item];
00093 array_splice( $newchain, 1, 0, $values );
00094 $values = $newchain;
00095 }
00096
00097 return $current;
00098 }
00099
00100 public function getData( &$group, $savedData ) {
00101 $defs = $this->readVariable( $group, 'en' );
00102 $code = $this->language;
00103
00104 $current = $savedData + $this->readVariable( $group, $code );
00105
00106
00107 $current = $this->cleanData( $defs, $current );
00108
00109 $chain = $current;
00110 if ( $this->chainable ) {
00111 foreach ( Language::getFallbacksFor( $code ) as $code ) {
00112 $fbdata = $this->readVariable( $group, $code );
00113 if ( $this->firstMagic ) {
00114 $fbdata = $this->cleanData( $defs, $fbdata );
00115 }
00116
00117 $chain = array_merge_recursive( $chain, $fbdata );
00118 }
00119 }
00120
00121 if ( $this->firstMagic ) {
00122 $chain = $this->mergeMagic( $defs, $chain );
00123 }
00124
00125 $data = $group['data'] = array( $defs, $chain, $current );
00126
00127 return $data;
00128 }
00129
00135 public function loadFromRequest( WebRequest $request ) {
00136 $saved = $this->parse( $this->formatForSave( $request ) );
00137 foreach ( $this->data as &$group ) {
00138 $this->getData( $group, $saved );
00139 }
00140 }
00141
00146 protected function getSavedData() {
00147 $data = TranslateUtils::getMessageContent( $this->databaseMsg, $this->language );
00148
00149 if ( !$data ) {
00150 return array();
00151 } else {
00152 return $this->parse( $data );
00153 }
00154 }
00155
00156 protected function parse( $data ) {
00157 $lines = array_map( 'trim', explode( "\n", $data ) );
00158 $array = array();
00159 foreach ( $lines as $line ) {
00160 if ( $line === '' || $line[0] === '#' || $line[0] === '<' ) {
00161 continue;
00162 }
00163
00164 if ( strpos( $line, '=' ) === false ) {
00165 continue;
00166 }
00167
00168 list( $name, $values ) = array_map( 'trim', explode( '=', $line, 2 ) );
00169 if ( $name === '' || $values === '' ) {
00170 continue;
00171 }
00172
00173 $data = array_map( 'trim', explode( ',', $values ) );
00174 $array[$name] = $data;
00175 }
00176
00177 return $array;
00178 }
00179
00185 protected function getIterator( $group ) {
00186 $groups = $this->getGroups();
00187
00188 return array_keys( $groups[$group]['data'][self::LANG_MASTER] );
00189 }
00190
00191 protected function val( $group, $type, $key ) {
00192 $array = $this->getGroups();
00193 wfSuppressWarnings();
00194 $subarray = $array[$group]['data'][$type][$key];
00195 wfRestoreWarnings();
00196 if ( $this->elementsInArray ) {
00197 if ( !$subarray || !count( $subarray ) ) {
00198 return array();
00199 }
00200 } else {
00201 if ( !$subarray ) {
00202 return array();
00203 }
00204 }
00205
00206 if ( !is_array( $subarray ) ) {
00207 $subarray = array( $subarray );
00208 }
00209
00210 return $subarray;
00211 }
00212
00218 protected function readVariable( $group, $code ) {
00219 $file = $group['file'];
00220 if ( !$group['code'] ) {
00221 $file = str_replace( '%CODE%', str_replace( '-', '_', ucfirst( $code ) ), $file );
00222 }
00223
00224 ${$group['var']} = array(); # Initialize
00225 if ( file_exists( $file ) ) {
00226 require $file; # Include
00227 }
00228
00229 if ( $group['code'] ) {
00230 wfSuppressWarnings();
00231 $data = (array) ${$group['var']} [$code];
00232 wfRestoreWarnings();
00233 } else {
00234 $data = ${$group['var']};
00235 }
00236
00237 return self::arrayMapRecursive( 'strval', $data );
00238 }
00239
00240 public static function arrayMapRecursive( $callback, $data ) {
00241 foreach ( $data as $index => $values ) {
00242 if ( is_array( $values ) ) {
00243 $data[$index] = self::arrayMapRecursive( $callback, $values );
00244 } else {
00245 $data[$index] = call_user_func( $callback, $values );
00246 }
00247 }
00248
00249 return $data;
00250 }
00251
00252 #
00253 # /Data retrieval
00254 #
00255
00256 #
00257 # Output
00258 #
00259 public function header( $title ) {
00260 $colspan = array( 'colspan' => 3 );
00261 $header = Xml::element( 'th', $colspan, $this->getTitle() . ' - ' . $title );
00262 $subheading[] = '<th>' . wfMessage( 'translate-magic-cm-original' )->escaped() . '</th>';
00263 $subheading[] = '<th>' . wfMessage( 'translate-magic-cm-current' )->escaped() . '</th>';
00264 $subheading[] = '<th>' . wfMessage( 'translate-magic-cm-to-be' )->escaped() . '</th>';
00265
00266 return '<tr>' . $header . '</tr>' .
00267 '<tr>' . implode( "\n", $subheading ) . '</tr>';
00268 }
00269
00270 public function output() {
00271 $colspan = array( 'colspan' => 3 );
00272
00273 $s = Xml::openElement( 'table', $this->tableAttributes );
00274
00275 foreach ( array_keys( $this->data ) as $group ) {
00276 $s .= $this->header( $this->data[$group]['label'] );
00277
00278 foreach ( $this->getIterator( $group ) as $key ) {
00279 $rowContents = '';
00280
00281 $value = $this->val( $group, self::LANG_MASTER, $key );
00282 if ( $this->firstMagic ) {
00283 array_shift( $value );
00284 }
00285
00286 $value = array_map( 'htmlspecialchars', $value );
00287
00288 $rowContents .= '<td dir="ltr">' . $this->formatElement( $value ) . '</td>';
00289
00290 $value = $this->val( $group, self::LANG_CHAIN, $key );
00291 if ( $this->firstMagic ) {
00292 array_shift( $value );
00293 }
00294
00295
00296
00297
00298 foreach ( $value as &$currentTranslation ) {
00299 $currentTranslation = Xml::element( 'bdi', null, $currentTranslation );
00300 }
00301 $value = $this->highlight( $key, $value );
00302 $rowContents .= '<td>' . $this->formatElement( $value ) . '</td>';
00303
00304 $value = $this->val( $group, self::LANG_CURRENT, $key );
00305 $rowContents .= '<td>';
00306 $rowContents .= $this->editElement( $key, $this->formatElement( $value ) );
00307 $rowContents .= '</td>';
00308
00309 $s .= Xml::tags( 'tr', array( 'id' => "mw-sp-magic-$key" ), $rowContents );
00310 }
00311 }
00312
00313 $context = RequestContext::getMain();
00314
00315 if ( $context->getUser()->isAllowed( 'translate' ) ) {
00316 $s .= '<tr>' . Xml::tags( 'td', $colspan, $this->getButtons() ) . '<tr>';
00317 }
00318
00319 $s .= Xml::closeElement( 'table' );
00320
00321 return Xml::tags(
00322 'form',
00323 array(
00324 'method' => 'post',
00325 'action' => $context->getRequest()->getRequestURL()
00326 ),
00327 $s
00328 );
00329 }
00330
00331 public function getButtons() {
00332 return Xml::inputLabel(
00333 wfMessage( 'translate-magic-cm-comment' )->text(),
00334 'comment',
00335 'sp-translate-magic-comment'
00336 ) .
00337 Xml::submitButton(
00338 wfMessage( 'translate-magic-cm-save' )->text(),
00339 array( 'name' => 'savetodb' )
00340 );
00341 }
00342
00343 public function formatElement( $element ) {
00344 if ( !count( $element ) ) {
00345 return '';
00346 }
00347
00348 if ( is_array( $element ) ) {
00349 $element = array_map( 'trim', $element );
00350 $element = implode( ', ', $element );
00351 }
00352
00353 return trim( $element );
00354 }
00355
00356 function getKeyForEdit( $key ) {
00357 return Sanitizer::escapeId( 'sp-translate-magic-cm-' . $this->id . $key );
00358 }
00359
00360 public function editElement( $key, $contents ) {
00361 return Xml::input( $this->getKeyForEdit( $key ), 40, $contents, array(
00362 'lang' => $this->language,
00363 'dir' => $this->targetDir,
00364 ) );
00365 }
00366
00367 #
00368 # /Output
00369 #
00370
00371 #
00372 # Save to database
00373 #
00374
00375 function getKeyForSave() {
00376 return $this->databaseMsg . '/' . $this->language;
00377 }
00378
00383 function formatForSave( WebRequest $request ) {
00384 $text = '';
00385
00386
00387 $replaceSpace = $request->getVal( 'module' ) !== 'magic';
00388
00389 foreach ( array_keys( $this->data ) as $group ) {
00390 foreach ( $this->getIterator( $group ) as $key ) {
00391 $data = $request->getText( $this->getKeyForEdit( $key ) );
00392
00393 $data = array_map( 'trim', explode( ',', $data ) );
00394
00395 if ( $replaceSpace ) {
00396
00397 $data = str_replace( ' ', '_', $data );
00398 }
00399
00400
00401 $data = implode( ', ', $data );
00402 if ( $data !== '' ) {
00403 $text .= "$key = $data\n";
00404 }
00405 }
00406 }
00407
00408 return $text;
00409 }
00410
00411 public function save( $request ) {
00412 $title = Title::newFromText( 'MediaWiki:' . $this->getKeyForSave() );
00413 $article = new Article( $title );
00414
00415 $data = "# DO NOT EDIT THIS PAGE DIRECTLY! Use [[Special:AdvancedTranslate]].\n<pre>\n" .
00416 $this->formatForSave( $request ) . "\n</pre>";
00417
00418 $comment = $request->getText(
00419 'comment',
00420 wfMessage( 'translate-magic-cm-updatedusing' )->inContentLanguage()->text()
00421 );
00422 $status = $article->doEdit( $data, $comment, 0 );
00423
00424 if ( $status === false || ( is_object( $status ) && !$status->isOK() ) ) {
00425 throw new MWException( wfMessage( 'translate-magic-cm-savefailed' )->text() );
00426 }
00427
00428
00429 $this->init = false;
00430 }
00431
00432 #
00433 # !Save to database
00434 #
00435
00436 #
00437 # Export
00438 #
00439 public function validate( &$errors = array(), $filter = false ) {
00440 $used = array();
00441 foreach ( array_keys( $this->data ) as $group ) {
00442 if ( $filter !== false && !in_array( $group, (array)$filter, true ) ) {
00443 continue;
00444 }
00445
00446 $this->validateEach( $errors, $group, $used );
00447 }
00448 }
00449
00450 protected function validateEach( &$errors = array(), $group, &$used ) {
00451 foreach ( $this->getIterator( $group ) as $key ) {
00452 $values = $this->val( $group, self::LANG_CURRENT, $key );
00453 $link = Xml::element( 'a', array( 'href' => "#mw-sp-magic-$key" ), $key );
00454
00455 if ( count( $values ) !== count( array_filter( $values ) ) ) {
00456 $errors[] = "There is empty value in $link.";
00457 }
00458
00459 foreach ( $values as $v ) {
00460 if ( isset( $used[$v] ) ) {
00461 $otherkey = $used[$v];
00462 $first = Xml::element(
00463 'a',
00464 array( 'href' => "#mw-sp-magic-$otherkey" ),
00465 $otherkey
00466 );
00467 $errors[] = "Translation <strong>$v</strong> is used more than once " .
00468 "for $first and $link.";
00469 } else {
00470 $used[$v] = $key;
00471 }
00472 }
00473 }
00474 }
00475
00476 public function export( $filter = false ) {
00477 $text = '';
00478 $errors = array();
00479 $this->validate( $errors, $filter );
00480 foreach ( $errors as $_ ) $text .= "#!!# $_\n";
00481
00482 foreach ( $this->getGroups() as $group => $data ) {
00483 if ( $filter !== false && !in_array( $group, (array)$filter, true ) ) {
00484 continue;
00485 }
00486
00487 $text .= $this->exportEach( $group, $data );
00488 }
00489
00490 return $text;
00491 }
00492
00493 protected function exportEach( $group, $data ) {
00494 $var = $data['var'];
00495 $items = $data['data'];
00496
00497 $extra = $data['code'] ? "['{$this->language}']" : '';
00498
00499 $out = '';
00500
00501 $indexKeys = array();
00502 foreach ( array_keys( $items[self::LANG_MASTER] ) as $key ) {
00503 $indexKeys[$key] = isset( $this->constants[$key] ) ?
00504 $this->constants[$key] :
00505 "'$key'";
00506 }
00507
00508 $padTo = max( array_map( 'strlen', $indexKeys ) ) + 3;
00509
00510 foreach ( $this->getIterator( $group ) as $key ) {
00511 $temp = "\t{$indexKeys[$key]}";
00512
00513 while ( strlen( $temp ) <= $padTo ) {
00514 $temp .= ' ';
00515 }
00516
00517 $from = self::LANG_CURRENT;
00518
00519 if ( $this->firstMagic ) {
00520 $from = self::LANG_CHAIN;
00521 }
00522
00523
00524 $val = $this->val( $group, self::LANG_CURRENT, $key );
00525 if ( !$val || !count( $val ) ) {
00526 continue;
00527 }
00528
00529
00530 $val = $this->val( $group, $from, $key );
00531
00532
00533
00534 $val = array_unique( $val );
00535
00536
00537 foreach ( $val as $k => $v ) {
00538 if ( $v === '' ) {
00539 unset( $val[$k] );
00540 }
00541 }
00542
00543
00544 if ( !count( $val ) ) {
00545 continue;
00546 }
00547
00548 $normalized = array_map( array( $this, 'normalize' ), $val );
00549 if ( $this->elementsInArray ) {
00550 $temp .= "=> array( " . implode( ', ', $normalized ) . " ),";
00551 } else {
00552 $temp .= "=> " . implode( ', ', $normalized ) . ",";
00553 }
00554 $out .= $temp . "\n";
00555 }
00556
00557 if ( $out !== '' ) {
00558 $text = "# {$data['label']} \n";
00559 $text .= "\$$var$extra = array(\n" . $out . ");\n\n";
00560
00561 return $text;
00562 } else {
00563 return '';
00564 }
00565 }
00566
00573 protected function normalize( $data ) {
00574 # Escape quotes
00575 if ( !is_string( $data ) ) {
00576 throw new MWException();
00577 }
00578 $data = preg_replace( "/(?<!\\\\)'/", "\'", trim( $data ) );
00579
00580 return "'$data'";
00581 }
00582
00583 #
00584 # /Export
00585 #
00586 public function highlight( $key, $values ) {
00587 return $values;
00588 }
00589 }
00590
00595 class SpecialPageAliasesCM extends ComplexMessages {
00596 protected $id = SpecialMagic::MODULE_SPECIAL;
00597 protected $databaseMsg = 'sp-translate-data-SpecialPageAliases';
00598 protected $chainable = true;
00599
00600 public function __construct( $code ) {
00601 parent::__construct( $code );
00602 $this->data['core'] = array(
00603 'label' => 'MediaWiki Core',
00604 'var' => 'specialPageAliases',
00605 'file' => Language::getMessagesFileName( '%CODE%' ),
00606 'code' => false,
00607 );
00608
00609 global $wgTranslateExtensionDirectory;
00610 $groups = MessageGroups::singleton()->getGroups();
00611 foreach ( $groups as $g ) {
00612 if ( !$g instanceof MediaWikiExtensionMessageGroup ) {
00613 continue;
00614 }
00615 $conf = $g->getConfiguration();
00616 if ( !isset( $conf['FILES']['aliasFile'] ) ) {
00617 continue;
00618 }
00619 $file = $conf['FILES']['aliasFile'];
00620
00621 $file = "$wgTranslateExtensionDirectory/$file";
00622 if ( file_exists( $file ) ) {
00623 $this->data[$g->getId()] = array(
00624 'label' => $g->getLabel(),
00625 'var' => 'specialPageAliases',
00626 'file' => $file,
00627 'code' => $code,
00628 );
00629 }
00630 }
00631 }
00632
00633 public function highlight( $key, $values ) {
00634 if ( count( $values ) ) {
00635 if ( !isset( $values[0] ) ) {
00636 throw new MWException( "Something missing from values: " .
00637 print_r( $values, true ) );
00638 }
00639
00640 $values[0] = "<strong>$values[0]</strong>";
00641 }
00642
00643 return $values;
00644 }
00645
00646 protected function validateEach( &$errors = array(), $group, &$used ) {
00647 parent::validateEach( $errors, $group, $used );
00648 foreach ( $this->getIterator( $group ) as $key ) {
00649 $values = $this->val( $group, self::LANG_CURRENT, $key );
00650
00651 foreach ( $values as $_ ) {
00652 wfSuppressWarnings();
00653 $title = SpecialPage::getTitleFor( $_ );
00654 wfRestoreWarnings();
00655 $link = Xml::element( 'a', array( 'href' => "#mw-sp-magic-$key" ), $key );
00656 if ( $title === null ) {
00657 if ( $_ !== '' ) {
00658
00659 $errors[] = "Translation <strong>$_</strong> is invalid title in $link.";
00660 }
00661 } else {
00662 $text = $title->getText();
00663 $dbkey = $title->getDBkey();
00664 if ( $text !== $_ && $dbkey !== $_ ) {
00665 $errors[] = "Translation <strong>$_</strong> for $link is not in " .
00666 "normalised form, which is <strong>$text</strong>";
00667 }
00668 }
00669 }
00670 }
00671 }
00672 }
00673
00678 class MagicWordsCM extends ComplexMessages {
00679 protected $id = SpecialMagic::MODULE_MAGIC;
00680 protected $firstMagic = true;
00681 protected $chainable = true;
00682 protected $databaseMsg = 'sp-translate-data-MagicWords';
00683
00684 public function __construct( $code ) {
00685 parent::__construct( $code );
00686 $this->data['core'] = array(
00687 'label' => 'MediaWiki Core',
00688 'var' => 'magicWords',
00689 'file' => Language::getMessagesFileName( '%CODE%' ),
00690 'code' => false,
00691 );
00692
00693 global $wgTranslateExtensionDirectory;
00694 $groups = MessageGroups::singleton()->getGroups();
00695 foreach ( $groups as $g ) {
00696 if ( !$g instanceof MediaWikiExtensionMessageGroup ) {
00697 continue;
00698 }
00699 $conf = $g->getConfiguration();
00700 if ( !isset( $conf['FILES']['magicFile'] ) ) {
00701 continue;
00702 }
00703 $file = $conf['FILES']['magicFile'];
00704
00705 $file = "$wgTranslateExtensionDirectory/$file";
00706 if ( file_exists( $file ) ) {
00707 $this->data[$g->getId()] = array(
00708 'label' => $g->getLabel(),
00709 'var' => 'magicWords',
00710 'file' => $file,
00711 'code' => $code,
00712 );
00713 }
00714 }
00715 }
00716
00717 public function highlight( $key, $values ) {
00718 if ( count( $values ) && $key === 'redirect' ) {
00719 $values[0] = "<strong>$values[0]</strong>";
00720 }
00721
00722 return $values;
00723 }
00724 }
00725
00730 class NamespaceCM extends ComplexMessages {
00731 protected $id = SpecialMagic::MODULE_NAMESPACE;
00732 protected $elementsInArray = false;
00733 protected $databaseMsg = 'sp-translate-data-Namespaces';
00734
00735 public function __construct( $code ) {
00736 parent::__construct( $code );
00737 $this->data['core'] = array(
00738 'label' => 'MediaWiki Core',
00739 'var' => 'namespaceNames',
00740 'file' => Language::getMessagesFileName( '%CODE%' ),
00741 'code' => false,
00742 );
00743 }
00744
00745 protected $constants = array(
00746 -2 => 'NS_MEDIA',
00747 -1 => 'NS_SPECIAL',
00748 0 => 'NS_MAIN',
00749 1 => 'NS_TALK',
00750 2 => 'NS_USER',
00751 3 => 'NS_USER_TALK',
00752 4 => 'NS_PROJECT',
00753 5 => 'NS_PROJECT_TALK',
00754 6 => 'NS_FILE',
00755 7 => 'NS_FILE_TALK',
00756 8 => 'NS_MEDIAWIKI',
00757 9 => 'NS_MEDIAWIKI_TALK',
00758 10 => 'NS_TEMPLATE',
00759 11 => 'NS_TEMPLATE_TALK',
00760 12 => 'NS_HELP',
00761 13 => 'NS_HELP_TALK',
00762 14 => 'NS_CATEGORY',
00763 15 => 'NS_CATEGORY_TALK',
00764 );
00765
00766 protected function validateEach( &$errors = array(), $group, &$used ) {
00767 parent::validateEach( $errors, $group, $used );
00768 foreach ( $this->getIterator( $group ) as $key ) {
00769 $values = $this->val( $group, self::LANG_CURRENT, $key );
00770
00771 if ( count( $values ) > 1 ) {
00772 $link = Xml::element( 'a', array( 'href' => "#mw-sp-magic-$key" ), $key );
00773 $errors[] = "Namespace $link can have only one translation. Replace the " .
00774 "translation with a new one, and notify staff about the change.";
00775 }
00776 }
00777 }
00778 }