diff options
Diffstat (limited to 'www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php')
-rw-r--r-- | www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php | 766 |
1 files changed, 766 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php b/www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php new file mode 100644 index 00000000..fcd8a314 --- /dev/null +++ b/www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php @@ -0,0 +1,766 @@ +<?php +/** + * Classes for complex messages (%MediaWiki special page aliases, namespace names, magic words). + * + * @file + * @author Niklas Laxström + * @copyright Copyright © 2008-2010, Niklas Laxström + * @license GPL-2.0-or-later + */ + +/** + * Base class which implements handling and translation interface of + * non-message %MediaWiki items. + * @todo Needs documentation. + */ +abstract class ComplexMessages { + const LANG_MASTER = 0; + const LANG_CHAIN = 1; + const LANG_CURRENT = 2; + const PLACEHOLDER = 'languagecodeplaceholder'; + + protected $language; + protected $targetHtmlCode; + protected $targetDir; + protected $id = '__BUG__'; + protected $variable = '__BUG__'; + protected $data = []; + protected $elementsInArray = true; + protected $databaseMsg = '__BUG__'; + protected $chainable = false; + protected $firstMagic = false; + protected $constants = []; + + protected $tableAttributes = [ + 'class' => 'wikitable', + 'border' => '2', + 'cellpadding' => '4', + 'cellspacing' => '0', + 'style' => 'background-color: #F9F9F9; border: 1px #AAAAAA solid; border-collapse: collapse;', + ]; + + public function __construct( $langCode ) { + $this->language = $langCode; + + $language = Language::factory( $langCode ); + $this->targetHtmlCode = $language->getHtmlCode(); + $this->targetDir = $language->getDir(); + } + + public function getTitle() { + // Give grep a chance to find the usages: + // translate-magic-special, translate-magic-words, translate-magic-namespace + return wfMessage( 'translate-magic-' . $this->id )->text(); + } + + // Data retrieval + protected $init = false; + + public function getGroups() { + if ( !$this->init ) { + $saved = $this->getSavedData(); + foreach ( $this->data as &$group ) { + $this->getData( $group, $saved ); + } + $this->init = true; + } + + return $this->data; + } + + public function cleanData( $defs, $current ) { + foreach ( $current as $item => $values ) { + if ( !$this->elementsInArray ) { + break; + } + + if ( !isset( $defs[$item] ) ) { + unset( $current[$item] ); + continue; + } + + foreach ( $values as $index => $value ) { + if ( in_array( $value, $defs[$item], true ) ) { + unset( $current[$item][$index] ); + } + } + } + + return $current; + } + + public function mergeMagic( $defs, $current ) { + foreach ( $current as $item => &$values ) { + $newchain = $defs[$item]; + array_splice( $newchain, 1, 0, $values ); + $values = $newchain; + } + + return $current; + } + + public function getData( &$group, $savedData ) { + $defs = $this->readVariable( $group, 'en' ); + $code = $this->language; + + $current = $savedData + $this->readVariable( $group, $code ); + + // Clean up duplicates to definitions from saved data + $current = $this->cleanData( $defs, $current ); + + $chain = $current; + if ( $this->chainable ) { + foreach ( Language::getFallbacksFor( $code ) as $code ) { + $fbdata = $this->readVariable( $group, $code ); + if ( $this->firstMagic ) { + $fbdata = $this->cleanData( $defs, $fbdata ); + } + + $chain = array_merge_recursive( $chain, $fbdata ); + } + } + + if ( $this->firstMagic ) { + $chain = $this->mergeMagic( $defs, $chain ); + } + + $data = $group['data'] = [ $defs, $chain, $current ]; + + return $data; + } + + /** + * Gets data from request. Needs to be run before the form is displayed and + * validation. Not needed for export, which uses request directly. + * @param WebRequest $request + */ + public function loadFromRequest( WebRequest $request ) { + $saved = $this->parse( $this->formatForSave( $request ) ); + foreach ( $this->data as &$group ) { + $this->getData( $group, $saved ); + } + } + + /** + * Gets saved data from Mediawiki namespace + * @return Array + */ + protected function getSavedData() { + $data = TranslateUtils::getMessageContent( $this->databaseMsg, $this->language ); + + if ( !$data ) { + return []; + } else { + return $this->parse( $data ); + } + } + + protected function parse( $data ) { + $lines = array_map( 'trim', explode( "\n", $data ) ); + $array = []; + foreach ( $lines as $line ) { + if ( $line === '' || $line[0] === '#' || $line[0] === '<' ) { + continue; + } + + if ( strpos( $line, '=' ) === false ) { + continue; + } + + list( $name, $values ) = array_map( 'trim', explode( '=', $line, 2 ) ); + if ( $name === '' || $values === '' ) { + continue; + } + + $data = array_map( 'trim', explode( ',', $values ) ); + $array[$name] = $data; + } + + return $array; + } + + /** + * Return an array of keys that can be used to iterate over all keys + * @param string $group + * @return Array of keys for data + */ + protected function getIterator( $group ) { + $groups = $this->getGroups(); + + return array_keys( $groups[$group]['data'][self::LANG_MASTER] ); + } + + protected function val( $group, $type, $key ) { + $array = $this->getGroups(); + Wikimedia\suppressWarnings(); + $subarray = $array[$group]['data'][$type][$key]; + Wikimedia\restoreWarnings(); + if ( $this->elementsInArray ) { + if ( !$subarray || !count( $subarray ) ) { + return []; + } + } else { + if ( !$subarray ) { + return []; + } + } + + if ( !is_array( $subarray ) ) { + $subarray = [ $subarray ]; + } + + return $subarray; + } + + /** + * @param string $group + * @param string $code + * @return array + */ + protected function readVariable( $group, $code ) { + $file = $group['file']; + if ( !$group['code'] ) { + $file = str_ireplace( self::PLACEHOLDER, str_replace( '-', '_', ucfirst( $code ) ), $file ); + } + + ${$group['var']} = []; # Initialize + if ( file_exists( $file ) ) { + require $file; # Include + } + + if ( $group['code'] ) { + Wikimedia\suppressWarnings(); + $data = (array)${$group['var']} [$code]; + Wikimedia\restoreWarnings(); + } else { + $data = ${$group['var']}; + } + + return self::arrayMapRecursive( 'strval', $data ); + } + + public static function arrayMapRecursive( $callback, $data ) { + foreach ( $data as $index => $values ) { + if ( is_array( $values ) ) { + $data[$index] = self::arrayMapRecursive( $callback, $values ); + } else { + $data[$index] = call_user_func( $callback, $values ); + } + } + + return $data; + } + + // Data retrieval + + // Output + public function header( $title ) { + $colspan = [ 'colspan' => 3 ]; + $header = Xml::element( 'th', $colspan, $this->getTitle() . ' - ' . $title ); + $subheading[] = '<th>' . wfMessage( 'translate-magic-cm-original' )->escaped() . '</th>'; + $subheading[] = '<th>' . wfMessage( 'translate-magic-cm-current' )->escaped() . '</th>'; + $subheading[] = '<th>' . wfMessage( 'translate-magic-cm-to-be' )->escaped() . '</th>'; + + return '<tr>' . $header . '</tr>' . + '<tr>' . implode( "\n", $subheading ) . '</tr>'; + } + + public function output() { + $colspan = [ 'colspan' => 3 ]; + + $s = Xml::openElement( 'table', $this->tableAttributes ); + + foreach ( array_keys( $this->data ) as $group ) { + $s .= $this->header( $this->data[$group]['label'] ); + + foreach ( $this->getIterator( $group ) as $key ) { + $rowContents = ''; + + $value = $this->val( $group, self::LANG_MASTER, $key ); + if ( $this->firstMagic ) { + array_shift( $value ); + } + + $value = array_map( 'htmlspecialchars', $value ); + // Force ltr direction. The source is pretty much guaranteed to be English-based. + $rowContents .= '<td dir="ltr">' . $this->formatElement( $value ) . '</td>'; + + $value = $this->val( $group, self::LANG_CHAIN, $key ); + if ( $this->firstMagic ) { + array_shift( $value ); + } + + // Apply bidi-isolation to each value. + // The values can both RTL and LTR and mixing them in a comma list + // can mix things up. + foreach ( $value as &$currentTranslation ) { + $currentTranslation = Xml::element( 'bdi', null, $currentTranslation ); + } + $value = $this->highlight( $key, $value ); + $rowContents .= '<td>' . $this->formatElement( $value ) . '</td>'; + + $value = $this->val( $group, self::LANG_CURRENT, $key ); + $rowContents .= '<td>'; + $rowContents .= $this->editElement( $key, $this->formatElement( $value ) ); + $rowContents .= '</td>'; + + $s .= Xml::tags( 'tr', [ 'id' => "mw-sp-magic-$key" ], $rowContents ); + } + } + + $context = RequestContext::getMain(); + + if ( $context->getUser()->isAllowed( 'translate' ) ) { + $s .= '<tr>' . Xml::tags( 'td', $colspan, $this->getButtons() ) . '<tr>'; + } + + $s .= Xml::closeElement( 'table' ); + + return Xml::tags( + 'form', + [ + 'method' => 'post', + 'action' => $context->getRequest()->getRequestURL() + ], + $s + ); + } + + public function getButtons() { + return Xml::inputLabel( + wfMessage( 'translate-magic-cm-comment' )->text(), + 'comment', + 'sp-translate-magic-comment' + ) . + Xml::submitButton( + wfMessage( 'translate-magic-cm-save' )->text(), + [ 'name' => 'savetodb' ] + ); + } + + public function formatElement( $element ) { + if ( !count( $element ) ) { + return ''; + } + + if ( is_array( $element ) ) { + $element = array_map( 'trim', $element ); + $element = implode( ', ', $element ); + } + + return trim( $element ); + } + + protected function getKeyForEdit( $key ) { + return Sanitizer::escapeId( 'sp-translate-magic-cm-' . $this->id . $key ); + } + + public function editElement( $key, $contents ) { + return Xml::input( $this->getKeyForEdit( $key ), 40, $contents, [ + 'lang' => $this->targetHtmlCode, + 'dir' => $this->targetDir, + ] ); + } + + // Output + + // Save to database + + protected function getKeyForSave() { + return $this->databaseMsg . '/' . $this->language; + } + + /** + * @param WebRequest $request + * @return string + */ + protected function formatForSave( WebRequest $request ) { + $text = ''; + + // Do not replace spaces by underscores for magic words. See bug T48613 + $replaceSpace = $request->getVal( 'module' ) !== 'magic'; + + foreach ( array_keys( $this->data ) as $group ) { + foreach ( $this->getIterator( $group ) as $key ) { + $data = $request->getText( $this->getKeyForEdit( $key ) ); + // Make a nice array out of the submit with trimmed values. + $data = array_map( 'trim', explode( ',', $data ) ); + + if ( $replaceSpace ) { + // Normalise: Replace spaces with underscores. + $data = str_replace( ' ', '_', $data ); + } + + // Create final format. + $data = implode( ', ', $data ); + if ( $data !== '' ) { + $text .= "$key = $data\n"; + } + } + } + + return $text; + } + + /** + * @param WebRequest $request + * @throws MWException + */ + public function save( $request ) { + $title = Title::newFromText( 'MediaWiki:' . $this->getKeyForSave() ); + $page = WikiPage::factory( $title ); + + $data = "# DO NOT EDIT THIS PAGE DIRECTLY! Use [[Special:AdvancedTranslate]].\n<pre>\n" . + $this->formatForSave( $request ) . "\n</pre>"; + + $comment = $request->getText( + 'comment', + wfMessage( 'translate-magic-cm-updatedusing' )->inContentLanguage()->text() + ); + + $content = ContentHandler::makeContent( $data, $title ); + $status = $page->doEditContent( $content, $comment ); + + if ( $status === false || ( is_object( $status ) && !$status->isOK() ) ) { + throw new MWException( wfMessage( 'translate-magic-cm-savefailed' )->text() ); + } + + /* Reset outdated array */ + $this->init = false; + } + + // Save to database + + // Export + public function validate( array &$errors, $filter = false ) { + $used = []; + foreach ( array_keys( $this->data ) as $group ) { + if ( $filter !== false && !in_array( $group, (array)$filter, true ) ) { + continue; + } + + $this->validateEach( $errors, $group, $used ); + } + } + + protected function validateEach( array &$errors, $group, &$used ) { + foreach ( $this->getIterator( $group ) as $key ) { + $values = $this->val( $group, self::LANG_CURRENT, $key ); + $link = Xml::element( 'a', [ 'href' => "#mw-sp-magic-$key" ], $key ); + + if ( count( $values ) !== count( array_filter( $values ) ) ) { + $errors[] = "There is empty value in $link."; + } + + foreach ( $values as $v ) { + if ( isset( $used[$v] ) ) { + $otherkey = $used[$v]; + $first = Xml::element( + 'a', + [ 'href' => "#mw-sp-magic-$otherkey" ], + $otherkey + ); + $errors[] = "Translation <strong>$v</strong> is used more than once " . + "for $first and $link."; + } else { + $used[$v] = $key; + } + } + } + } + + public function export( $filter = false ) { + $text = ''; + $errors = []; + $this->validate( $errors, $filter ); + foreach ( $errors as $_ ) { + $text .= "#!!# $_\n"; + } + + foreach ( $this->getGroups() as $group => $data ) { + if ( $filter !== false && !in_array( $group, (array)$filter, true ) ) { + continue; + } + + $text .= $this->exportEach( $group, $data ); + } + + return $text; + } + + protected function exportEach( $group, $data ) { + $var = $data['var']; + $items = $data['data']; + + $extra = $data['code'] ? "['{$this->language}']" : ''; + + $out = ''; + + $indexKeys = []; + foreach ( array_keys( $items[self::LANG_MASTER] ) as $key ) { + $indexKeys[$key] = $this->constants[$key] ?? "'$key'"; + } + + $padTo = max( array_map( 'strlen', $indexKeys ) ) + 3; + + foreach ( $this->getIterator( $group ) as $key ) { + $temp = "\t{$indexKeys[$key]}"; + + while ( strlen( $temp ) <= $padTo ) { + $temp .= ' '; + } + + $from = self::LANG_CURRENT; + // Abuse of the firstMagic property, should use something proper + if ( $this->firstMagic ) { + $from = self::LANG_CHAIN; + } + + // Check for translations + $val = $this->val( $group, self::LANG_CURRENT, $key ); + if ( !$val || !count( $val ) ) { + continue; + } + + // Then get the data we really want + $val = $this->val( $group, $from, $key ); + + // Remove duplicated entries, causes problems with magic words + // Just to be sure, it should not be possible to save invalid data anymore + $val = array_unique( $val /* @todo SORT_REGULAR */ ); + + // So do empty elements... + foreach ( $val as $k => $v ) { + if ( $v === '' ) { + unset( $val[$k] ); + } + } + + // Another check + if ( !count( $val ) ) { + continue; + } + + $normalized = array_map( [ $this, 'normalize' ], $val ); + if ( $this->elementsInArray ) { + $temp .= '=> array( ' . implode( ', ', $normalized ) . ' ),'; + } else { + $temp .= '=> ' . implode( ', ', $normalized ) . ','; + } + $out .= $temp . "\n"; + } + + if ( $out !== '' ) { + $text = "# {$data['label']} \n"; + $text .= "\$$var$extra = array(\n" . $out . ");\n\n"; + + return $text; + } else { + return ''; + } + } + + /** + * Returns string with quotes that should be valid php + * @param string $data + * @throws MWException + * @return string + */ + protected function normalize( $data ) { + # Escape quotes + if ( !is_string( $data ) ) { + throw new MWException(); + } + $data = preg_replace( "/(?<!\\\\)'/", "\'", trim( $data ) ); + + return "'$data'"; + } + + // Export + public function highlight( $key, $values ) { + return $values; + } +} + +/** + * Adds support for translating special page aliases via Special:AdvancedTranslate. + * @todo Needs documentation. + */ +class SpecialPageAliasesCM extends ComplexMessages { + protected $id = SpecialMagic::MODULE_SPECIAL; + protected $databaseMsg = 'sp-translate-data-SpecialPageAliases'; + protected $chainable = true; + + public function __construct( $code ) { + parent::__construct( $code ); + $this->data['core'] = [ + 'label' => 'MediaWiki Core', + 'var' => 'specialPageAliases', + 'file' => Language::getMessagesFileName( self::PLACEHOLDER ), + 'code' => false, + ]; + + $groups = MessageGroups::singleton()->getGroups(); + foreach ( $groups as $g ) { + if ( !$g instanceof MediaWikiExtensionMessageGroup ) { + continue; + } + $conf = $g->getConfiguration(); + if ( !isset( $conf['FILES']['aliasFileSource'] ) ) { + continue; + } + $file = $g->replaceVariables( $conf['FILES']['aliasFileSource'], 'en' ); + if ( file_exists( $file ) ) { + $this->data[$g->getId()] = [ + 'label' => $g->getLabel(), + 'var' => 'specialPageAliases', + 'file' => $file, + 'code' => $code, + ]; + } + } + } + + public function highlight( $key, $values ) { + if ( count( $values ) ) { + if ( !isset( $values[0] ) ) { + throw new MWException( 'Something missing from values: ' . + print_r( $values, true ) ); + } + + $values[0] = "<strong>$values[0]</strong>"; + } + + return $values; + } + + protected function validateEach( array &$errors, $group, &$used ) { + parent::validateEach( $errors, $group, $used ); + foreach ( $this->getIterator( $group ) as $key ) { + $values = $this->val( $group, self::LANG_CURRENT, $key ); + + foreach ( $values as $_ ) { + Wikimedia\suppressWarnings(); + $title = SpecialPage::getTitleFor( $_ ); + Wikimedia\restoreWarnings(); + $link = Xml::element( 'a', [ 'href' => "#mw-sp-magic-$key" ], $key ); + if ( $title === null ) { + if ( $_ !== '' ) { + // Empty values checked elsewhere + $errors[] = "Translation <strong>$_</strong> is invalid title in $link."; + } + } else { + $text = $title->getText(); + $dbkey = $title->getDBkey(); + if ( $text !== $_ && $dbkey !== $_ ) { + $errors[] = "Translation <strong>$_</strong> for $link is not in " . + "normalised form, which is <strong>$text</strong>"; + } + } + } + } + } +} + +/** + * Adds support for translating magic words via Special:AdvancedTranslate. + * @todo Needs documentation. + */ +class MagicWordsCM extends ComplexMessages { + protected $id = SpecialMagic::MODULE_MAGIC; + protected $firstMagic = true; + protected $chainable = true; + protected $databaseMsg = 'sp-translate-data-MagicWords'; + + public function __construct( $code ) { + parent::__construct( $code ); + $this->data['core'] = [ + 'label' => 'MediaWiki Core', + 'var' => 'magicWords', + 'file' => Language::getMessagesFileName( self::PLACEHOLDER ), + 'code' => false, + ]; + + $groups = MessageGroups::singleton()->getGroups(); + foreach ( $groups as $g ) { + if ( !$g instanceof MediaWikiExtensionMessageGroup ) { + continue; + } + $conf = $g->getConfiguration(); + if ( !isset( $conf['FILES']['magicFileSource'] ) ) { + continue; + } + $file = $g->replaceVariables( $conf['FILES']['magicFileSource'], 'en' ); + if ( file_exists( $file ) ) { + $this->data[$g->getId()] = [ + 'label' => $g->getLabel(), + 'var' => 'magicWords', + 'file' => $file, + 'code' => $code, + ]; + } + } + } + + public function highlight( $key, $values ) { + if ( count( $values ) && $key === 'redirect' ) { + $values[0] = "<strong>$values[0]</strong>"; + } + + return $values; + } +} + +/** + * Adds support for translating namespace names via Special:AdvancedTranslate. + * @todo Needs documentation. + */ +class NamespaceCM extends ComplexMessages { + protected $id = SpecialMagic::MODULE_NAMESPACE; + protected $elementsInArray = false; + protected $databaseMsg = 'sp-translate-data-Namespaces'; + + public function __construct( $code ) { + parent::__construct( $code ); + $this->data['core'] = [ + 'label' => 'MediaWiki Core', + 'var' => 'namespaceNames', + 'file' => Language::getMessagesFileName( self::PLACEHOLDER ), + 'code' => false, + ]; + } + + protected $constants = [ + -2 => 'NS_MEDIA', + -1 => 'NS_SPECIAL', + 0 => 'NS_MAIN', + 1 => 'NS_TALK', + 2 => 'NS_USER', + 3 => 'NS_USER_TALK', + 4 => 'NS_PROJECT', + 5 => 'NS_PROJECT_TALK', + 6 => 'NS_FILE', + 7 => 'NS_FILE_TALK', + 8 => 'NS_MEDIAWIKI', + 9 => 'NS_MEDIAWIKI_TALK', + 10 => 'NS_TEMPLATE', + 11 => 'NS_TEMPLATE_TALK', + 12 => 'NS_HELP', + 13 => 'NS_HELP_TALK', + 14 => 'NS_CATEGORY', + 15 => 'NS_CATEGORY_TALK', + ]; + + protected function validateEach( array &$errors, $group, &$used ) { + parent::validateEach( $errors, $group, $used ); + foreach ( $this->getIterator( $group ) as $key ) { + $values = $this->val( $group, self::LANG_CURRENT, $key ); + + if ( count( $values ) > 1 ) { + $link = Xml::element( 'a', [ 'href' => "#mw-sp-magic-$key" ], $key ); + $errors[] = "Namespace $link can have only one translation. Replace the " . + 'translation with a new one, and notify staff about the change.'; + } + } + } +} |