Jarry1250 has uploaded a new change for review. https://gerrit.wikimedia.org/r/156129
Change subject: Remove SVGFormatReader class and distribute its methods ...................................................................... Remove SVGFormatReader class and distribute its methods * Anything to do with the DOM goes into a new class, SVGFile. Note that this class generally has no dependency on a wiki-based storage facility, though it does have a factory method to cover that usage. * SVGFormatWriter gains getPreferredTranslations, since only it uses such a method anyway. * SVGMessageGroup becomes the owner of all things wiki-related, i.e. getOnWikiTranslations(). This adapts TranslateSvg to fit a more test-drive paradigm where the pretty complicated logic of SVGFile becomes easily testable without recourse to the creation of endless message groups, which is time and memory intensive (initial reports suggest 95% time saving). Also, take advantage of this change to tidy up a number of internal APIs. Kudos to Antoine Musso for the inspiration. Change-Id: Ia2fc5cb8e07787c6039e1c9520a7b6f33284f3a4 --- R SVGFile.php M SVGFormatWriter.php M SVGMessageGroup.php M TranslateSvg.php M TranslateSvgHooks.php M TranslateSvgTasks.php R tests/phpunit/SVGFileTest.php 7 files changed, 370 insertions(+), 332 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/TranslateSvg refs/changes/29/156129/1 diff --git a/SVGFormatReader.php b/SVGFile.php similarity index 61% rename from SVGFormatReader.php rename to SVGFile.php index 5cb5637..0c29083 100644 --- a/SVGFormatReader.php +++ b/SVGFile.php @@ -1,100 +1,89 @@ <?php /** - * This file contains classes for reading and manipulating the content of SVG files. + * This file contains classes for manipulating the contents on an SVG file. + * Intended to include all references to PHP's DOM manipulation system. * * @file * @author Harry Burt - * @copyright Copyright © 2012 Harry Burt + * @copyright Copyright © 2014 Harry Burt * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */ - -/** - * Class for reading and manipulating the content of SVG files. - * @seealso SVGFormatWriter - */ -class SVGFormatReader { - - /** - * @var MessageGroup - */ - protected $group; - +class SVGFile { /** * @var DOMDocument */ - protected $svg; + private $document; /** * @var DOMXpath */ protected $xpath = null; - protected $started = array(); - protected $expanded = array(); - protected $filteredTextNodes = array(); - protected $savedLanguages = array(); protected $isTranslationReady = false; - - protected $inProgressTranslations = array(); - protected $inFileTranslations = null; - protected $onWikiTranslations = null; + protected $savedLanguages; + protected $inFileTranslations; + protected $filteredTextNodes; + protected $fallbackLanguage; /** - * Initialise a new SVGFormatReader from an SVGMessageGroup and an optional array of translation overrides + * Construct an SVGFile object. * - * @param SVGMessageGroup $group - * @param array $inProgressTranslations Optional array of translation overrides to be folded in later - * @throws MWException if file not found + * @seealso self::newFromMessageGroup + * @param string $path + * @param string $fallbackLanguage + * @todo Handle DOM warnings */ - public function __construct( SVGMessageGroup $group, $inProgressTranslations = array() ) { - $this->group = $group; - $this->inProgressTranslations = $inProgressTranslations; + public function __construct( $path, $fallbackLanguage ){ + // Save sourceLanguage for later (mostly so we can understand which language is the fallback) + $this->fallbackLanguage = $fallbackLanguage; - $title = Title::makeTitleSafe( NS_FILE, $this->group->getId() ); - $file = wfFindFile( $title ); - if ( !$file || !$file->exists() ) { - // Double-check it definitely exists - throw new MWException( 'File not found' ); - } - - $this->svg = new DOMDocument( '1.0' ); + $this->document = new DOMDocument( '1.0' ); // Warnings need to be suppressed in case there are DOM warnings wfSuppressWarnings(); - $this->svg->load( $file->getLocalRefPath() ); - $this->xpath = new DOMXpath( $this->svg ); + $this->document->load( $path ); + $this->xpath = new DOMXpath( $this->document ); wfRestoreWarnings(); $this->xpath->registerNamespace( 'svg', 'http://www.w3.org/2000/svg' ); - if ( !$this->makeTranslationReady() ) { - throw new MWException( 'file not found' ); - } + + // $this->isTranslationReady() can be used to test if construction was a success + $this->makeTranslationReady(); } /** - * Makes $this->svg ready for translation by inserting <switch> tags where they need to be, etc. + * Was the file successfully made translation ready i.e. is it translatable? + * + * @return boolean + */ + public function isTranslationReady() { + return $this->isTranslationReady; + } + + /** + * Makes $this->document ready for translation by inserting <switch> tags where they need to be, etc. * Also works as a check on the compatibility of the file since it will return false if it fails. * * @todo: Find a way of making isTranslationReady a proper check * @todo: add interlanguage consistency check - * @return bool False on failure, true on success + * @return bool False on failure, DOMDocument on success */ protected function makeTranslationReady() { - if ( $this->isTranslationReady ) { + if( $this->isTranslationReady ) { return true; } - if ( $this->svg->documentElement === null ) { + if ( $this->document->documentElement === null ) { // Empty or malformed file return false; } // Automated editors have a habit of using XML entity references in the SVG namespace // declaration or simply forgetting to set one at all. Both need to be fixed. - $defaultNS = $this->svg->documentElement->lookupnamespaceURI( null ); + $defaultNS = $this->document->documentElement->lookupnamespaceURI( null ); if ( $defaultNS === null || preg_match( '/^(&[^;]+;)+$/', $defaultNS, $match ) ) { // Bad or nonexistent default namespace set, fill in sensible default - $this->svg->documentElement->setAttributeNS( + $this->document->documentElement->setAttributeNS( 'http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg' @@ -102,14 +91,14 @@ $defaultNS = 'http://www.w3.org/2000/svg'; } - $texts = $this->svg->getElementsByTagName( 'text' ); + $texts = $this->document->getElementsByTagName( 'text' ); $textLength = $texts->length; if ( $textLength === 0 ) { // Nothing to translate! return false; } - $styles = $this->svg->getElementsByTagName( 'style' ); + $styles = $this->document->getElementsByTagName( 'style' ); $styleLength = $styles->length; for ( $i = 0; $i < $styleLength; $i++ ) { $style = $styles->item( $i ); @@ -129,7 +118,7 @@ } } - if ( $this->svg->getElementsByTagName( 'tref' )->length !== 0 ) { + if ( $this->document->getElementsByTagName( 'tref' )->length !== 0 ) { // Tref tags not (yet) supported return false; } @@ -137,8 +126,8 @@ // Strip empty tspans, texts, fill $idsInUse $idsInUse = array( 0 ); $translatableNodes = array(); - $tspans = $this->svg->getElementsByTagName( 'tspan' ); - $texts = $this->svg->getElementsByTagName( 'text' ); + $tspans = $this->document->getElementsByTagName( 'tspan' ); + $texts = $this->document->getElementsByTagName( 'text' ); foreach ( $tspans as $tspan ) { if ( $tspan->childNodes->length > 1 ) { return false; // Nested tspans not (yet) supported @@ -165,6 +154,7 @@ } } if ( !$translatableNode->hasChildNodes() ) { + // Empty tag, will just confuse translators if we leave it in $translatableNode->parentNode->removeChild( $translatableNode ); } } @@ -178,7 +168,7 @@ } } - $textLength = $this->svg->getElementsByTagName( 'text' )->length; + $textLength = $this->document->getElementsByTagName( 'text' )->length; for ( $i = 0; $i < $textLength; $i++ ) { /** @var DOMElement $text */ $text = $texts->item( $i ); @@ -221,7 +211,7 @@ } } } else { - $switch = $this->svg->createElementNS( $defaultNS, 'switch' ); + $switch = $this->document->createElementNS( $defaultNS, 'switch' ); $text->parentNode->insertBefore( $switch, $text ); // Move node into new sibling <switch> element $switch->appendChild( $text ); @@ -251,181 +241,24 @@ $text->parentNode->setAttribute( 'style', $style ); } } + $this->isTranslationReady = true; return true; } - /* - * Collate and prepare an array of translations from multiple sources: - * in file, on wiki, $this->filteredTextNodes and in-progress. - * - * return array Array of translations - */ - protected function getPreferredTranslations() { - $inFileTranslations = $this->getInFileTranslations(); - $onWikiTranslations = $this->getOnWikiTranslations(); - $inProgressTranslations = $this->getInProgressTranslations(); - - // Collapse in-progress translations into on-wiki translations - foreach ( $inProgressTranslations as $key => $languages ) { - foreach ( $languages as $language => $translation ) { - $language = ( $this->group->getSourceLanguage() === $language ) ? 'fallback' : $language; - $onWikiTranslations[$key][$language] = TranslateSvgUtils::translationToArray( $translation ); - } - } - - // Collapse on-wiki translations translations into in-progress translations - foreach ( $onWikiTranslations as $key => $languages ) { - foreach ( $languages as $language => $translation ) { - $oldItem = isset( $inFileTranslations[$key][$language] ) ? $inFileTranslations[$key][$language] : array(); - $inFileTranslations[$key][$language] = $onWikiTranslations[$key][$language] + $oldItem; - if ( $language !== 'fallback' ) { - $inFileTranslations[$key][$language]['id'] = $inFileTranslations[$key]['fallback']['id'] . "-$language"; - } - } - } - - // "Unfilter" translations - $inFileTranslations = array_merge( $inFileTranslations, $this->filteredTextNodes ); - - // Ensure that child tspan translations prompt new <text>s to be created - // by duplicating the fallback version. - foreach ( $inFileTranslations as $languages ) { - foreach ( $languages as $language => $translation ) { - if ( isset( $languages['fallback']['data-parent'] ) ) { - $parent = $languages['fallback']['data-parent']; - $inFileTranslations[$parent][$language] = $inFileTranslations[$parent]['fallback']; - if ( $language !== 'fallback' ) { - $inFileTranslations[$parent][$language]['id'] .= "-$language"; - } - } - } - } - - return $inFileTranslations; - } - - /** - * Get the array of in-progress translations - * @return array - */ - public function getInProgressTranslations() { - return $this->inProgressTranslations; - } /* - * Compile and return an update version of the SVG, including all new translations. + * Analyse the SVG file, extracting translations and other metadata. Expects the file to + * be in a certain format: see self::makeTranslationReady() for details. * - * @return DOMDocument New SVG file - */ - public function getSVG() { - $translations = $this->getPreferredTranslations(); - $currentLanguages = $this->getSavedLanguages(); - $switches = $this->svg->getElementsByTagName( 'switch' ); - $number = $switches->length; - - for ( $i = 0; $i < $number; $i++ ) { - $switch = $switches->item( $i ); - $fallback = $this->xpath->query( - "text[not(@systemLanguage)]|svg:text[not(@systemLanguage)]", $switch - ); - if ( $fallback->length === 0 ) { - // Some sort of deep hierarchy, can't translate - continue; - } - - /** @var DOMElement $fallbackText */ - $fallbackText = $fallback->item( 0 ); - $textId = $fallbackText->getAttribute( 'id' ); - - foreach ( $translations[$textId] as $language => $translation ) { - // Sort out systemLanguage attribute - if ( $language !== 'fallback' ) { - if ( strpos( $language, '-' ) !== false ) { - list( $before, $after ) = explode( '-', $language ); - $language = $before . '_' . strtoupper( $after ); - } - $translation['systemLanguage'] = $language; - } - - // Prepare an array of "children" (sub-messages) - $children = array(); - if ( isset( $translation['data-children'] ) ) { - $children = explode( '|', $translation['data-children'] ); - foreach ( $children as &$child ) { - if ( isset( $translations[$child][$language] ) ) { - $child = $translations[$child][$language]; - } else { - $child = $translations[$child]['fallback']; - } - $child = TranslateSvgUtils::arrayToNode( $child, $this->svg, 'tspan' ); - } - } - - // Set up text tag - $text = $translation['text']; - unset( $translation['text'] ); - $newTextTag = TranslateSvgUtils::arrayToNode( $translation, $this->svg, 'text' ); - - // Add text, replacing $1, $2 etc. with translations - TranslateSvgUtils::replaceIndicesRecursive( $text, $children, $this->svg, $newTextTag ); - - // Put text tag into document - $path = ( $language === 'fallback' ) ? - "svg:text[not(@systemLanguage)]|text[not(@systemLanguage)]" : - "svg:text[@systemLanguage='$language']|text[@systemLanguage='$language']"; - $existing = $this->xpath->query( $path, $switch ); - if ( $existing->length == 1 ) { - // Only one matching text node, replace - $switch->replaceChild( $newTextTag, $existing->item( 0 ) ); - } elseif ( $existing->length == 0 ) { - // No matching text node for this language, so we'll create one - $switch->appendChild( $newTextTag ); - } - $langName = ( $language === 'fallback' ) ? - 'fallback' : Language::fetchLanguageName( $language ); - if ( in_array( $language, $currentLanguages ) ) { - $this->expanded[$langName] = 'expanded'; - } else { - $this->started[$langName] = 'started'; - } - } - } - // Move sublocales to the beginning of their switch elements - $sublocales = $this->xpath->query( - "//text[contains(@systemLanguage,'_')]" . "|" . "//svg:text[contains(@systemLanguage,'_')]" - ); - $count = $sublocales->length; - for ( $i = 0; $i < $count; $i++ ) { - $firstSibling = $sublocales->item( $i )->parentNode->childNodes->item( 0 ); - $sublocales->item( $i )->parentNode->insertBefore( $sublocales->item( $i ), $firstSibling ); - } - // Move fallbacks to the end of their switch elements - $fallbacks = $this->xpath->query( - "//text[not(@systemLanguage)]" . "|" . "//svg:text[not(@systemLanguage)]" - ); - $count = $fallbacks->length; - for ( $i = 0; $i < $count; $i++ ) { - $fallbacks->item( $i )->parentNode->appendChild( $fallbacks->item( $i ) ); - } - return $this->svg; - } - - /* - * Extract translations from the SVG file - * - * @param bool $forceUpdate Force the regeneration the list (default: false) * @return array Array of translations (indexed by ID, then langcode, then property) */ - public function getInFileTranslations( $forceUpdate = false ) { - if ( $this->inFileTranslations !== null && !$forceUpdate ) { - return $this->inFileTranslations; - } - - $switches = $this->svg->getElementsByTagName( 'switch' ); + protected function analyse() { + $switches = $this->document->getElementsByTagName( 'switch' ); $number = $switches->length; $translations = array(); $this->filteredTextNodes = array(); // Reset + for ( $i = 0; $i < $number; $i++ ) { /** @var DOMElement $switch */ $switch = $switches->item( $i ); @@ -481,94 +314,59 @@ } // Replace with $1, $2 etc. - $text->replaceChild( $this->svg->createTextNode( '$' . $counter ), $child ); + $text->replaceChild( $this->document->createTextNode( '$' . $counter ), $child ); $counter++; } } - foreach( $realLangs as $realLang ) { - if ( $hasActualTextContent ) { - // @todo: work out what this does - $translations[$textId][$realLang] = TranslateSvgUtils::nodeToArray( $text ); - } else { - $this->filteredTextNodes[$textId][$realLang] = TranslateSvgUtils::nodeToArray( $text ); - } - $savedLang = ( $realLang === 'fallback' ) ? $this->group->getSourceLanguage() : $realLang; - $this->savedLanguages[] = $savedLang; + if ( $hasActualTextContent ) { + // If the <text> has *its own* text content, rather than just <tspan>s, register it + // for translation. + $translations[$textId][$langCode] = TranslateSvgUtils::nodeToArray( $text ); + } else { + $this->filteredTextNodes[$textId][$langCode] = TranslateSvgUtils::nodeToArray( $text ); } + $savedLang = ( $langCode === 'fallback' ) ? $this->fallbackLanguage : $langCode; + $this->savedLanguages[] = $savedLang; } } $this->inFileTranslations = $translations; $this->savedLanguages = array_unique( $this->savedLanguages ); + } + + /** + * Try to return $this->inFileTranslations. If it is not cached, analyse the SVG + * and hence generate it. + * + * @return array + */ + public function getInFileTranslations() { + if ( $this->inFileTranslations === null ) { + $this->analyse(); + } return $this->inFileTranslations; } - /* - * Extract translations from on wiki + /** + * Try to return $this->savedLanguages (a list of languages which have one or more + * translations in-file). If it is not cached, analyse the SVG and hence generate it. * - * @param bool $forceUpdate Force the regeneration the list (default: false) - * @return array Array of translations (indexed by ID, then langcode, then property) - */ - public function getOnWikiTranslations( $forceUpdate = false ) { - if( $this->onWikiTranslations !== null && !$forceUpdate ) { - return $this->onWikiTranslations; - } - - $onWikiTranslations = array(); - $languages = $this->group->getOnWikiLanguages(); - - // Translations generated onwiki - foreach ( $languages as $language ) { - $collection = $this->group->initCollection( $language ); - $collection->loadTranslations(); - $mangler = $this->group->getMangler(); - foreach ( $collection as $item ) { - /** @var TMessage $item */ - $key = explode( '/', $mangler->unMangle( $item->key() ) ); - $key = array_pop( $key ); - $translation = str_replace( TRANSLATE_FUZZY, '', $item->translation() ); - - if ( $translation === '' ) { - // No translation provided - continue; - } - - $language = ( $this->group->getSourceLanguage() == $language ) ? 'fallback' : $language; - $item = TranslateSvgUtils::translationToArray( $translation ); - if ( !isset( $onWikiTranslations[$key] ) ) { - $onWikiTranslations[$key] = array(); - } - $onWikiTranslations[$key][$language] = $item; - } - } - - $this->onWikiTranslations = $onWikiTranslations; - return $this->onWikiTranslations; - } - - /* - * Get a list of languages which have one or more translations in file - * - * @return array Array of languages + * @return array */ public function getSavedLanguages() { - $this->getInFileTranslations(); - - // $this->savedLanguages is set by $this->getInFileTranslations(), - // which handles caching. + if ( $this->savedLanguages === null ) { + $this->analyse(); + } return $this->savedLanguages; } /* - * Get a list of languages which have one or more translations in file + * Get a list of languages which have one or more translations in-file * * @return array Array of languages, split into 'full' and 'partial' subarrays */ public function getSavedLanguagesFiltered() { $translations = $this->getInFileTranslations(); - - // $this->savedLanguages is set by $this->getInFileTranslations(), - // which handles caching. - $savedLanguages = $this->savedLanguages; + $savedLanguages = $this->getSavedLanguages(); $full = array(); $partial = array(); @@ -580,7 +378,7 @@ break; } } - if ( $fullSoFar || $savedLanguage == $this->group->getSourceLanguage() ) { + if ( $fullSoFar || $savedLanguage == $this->fallbackLanguage ) { $full[] = $savedLanguage; } else { $partial[] = $savedLanguage; @@ -589,21 +387,162 @@ return array( 'full' => $full, 'partial' => $partial ); } - /* - * Get array of languages which were started in the last SVG update + /** + * Try to return $this->filteredTextNodes (an array of <text> nodes that contain only + * child elements). If it is not cached, analyse the SVG and hence generate it. * - * @return array Array of languages + * @return array */ - public function getStarted() { - return $this->started; + public function getFilteredTextNodes() { + if ( $this->filteredTextNodes === null ) { + $this->analyse(); + } + return $this->filteredTextNodes; } /* - * Get array of languages which were added to in the last SVG update + * Compile an updated DOM model of the SVG using the provided set of translations * - * @return array Array of languages + * @return array Array with keys 'expanded' and 'started', each an array of language names */ - public function getExpanded() { - return $this->expanded; + public function switchToTranslationSet( $translations ) { + $currentLanguages = $this->getSavedLanguages(); + $expanded = $started = array(); + + $switches = $this->document->getElementsByTagName( 'switch' ); + $number = $switches->length; + for ( $i = 0; $i < $number; $i++ ) { + $switch = $switches->item( $i ); + $fallback = $this->xpath->query( + "text[not(@systemLanguage)]|svg:text[not(@systemLanguage)]", $switch + ); + if ( $fallback->length === 0 ) { + // Some sort of deep hierarchy, can't translate + continue; + } + + /** @var DOMElement $fallbackText */ + $fallbackText = $fallback->item( 0 ); + $textId = $fallbackText->getAttribute( 'id' ); + + foreach ( $translations[$textId] as $language => $translation ) { + // Sort out systemLanguage attribute + if ( $language !== 'fallback' ) { + if ( strpos( $language, '-' ) !== false ) { + list( $before, $after ) = explode( '-', $language ); + $language = $before . '_' . strtoupper( $after ); + } + $translation['systemLanguage'] = $language; + } + + // Prepare an array of "children" (sub-messages) + $children = array(); + if ( isset( $translation['data-children'] ) ) { + $children = explode( '|', $translation['data-children'] ); + foreach ( $children as &$child ) { + if ( isset( $translations[$child][$language] ) ) { + $child = $translations[$child][$language]; + } else { + $child = $translations[$child]['fallback']; + } + $child = TranslateSvgUtils::arrayToNode( $child, $this->document, 'tspan' ); + } + } + + // Set up text tag + $text = $translation['text']; + unset( $translation['text'] ); + $newTextTag = TranslateSvgUtils::arrayToNode( $translation, $this->document, 'text' ); + + // Add text, replacing $1, $2 etc. with translations + TranslateSvgUtils::replaceIndicesRecursive( $text, $children, $this->document, $newTextTag ); + + // Put text tag into document + $path = ( $language === 'fallback' ) ? + "svg:text[not(@systemLanguage)]|text[not(@systemLanguage)]" : + "svg:text[@systemLanguage='$language']|text[@systemLanguage='$language']"; + $existing = $this->xpath->query( $path, $switch ); + if ( $existing->length == 1 ) { + // Only one matching text node, replace + $switch->replaceChild( $newTextTag, $existing->item( 0 ) ); + } elseif ( $existing->length == 0 ) { + // No matching text node for this language, so we'll create one + $switch->appendChild( $newTextTag ); + } + + $langName = ( $language === 'fallback' ) ? + 'fallback' : Language::fetchLanguageName( $language ); + if ( in_array( $language, $currentLanguages ) ) { + $expanded[] = $langName; + } else { + $started[] = $langName; + } + } + } + + // Move sublocales to the beginning of their switch elements + $sublocales = $this->xpath->query( + "//text[contains(@systemLanguage,'_')]" . "|" . "//svg:text[contains(@systemLanguage,'_')]" + ); + $count = $sublocales->length; + for ( $i = 0; $i < $count; $i++ ) { + $firstSibling = $sublocales->item( $i )->parentNode->childNodes->item( 0 ); + $sublocales->item( $i )->parentNode->insertBefore( $sublocales->item( $i ), $firstSibling ); + } + + // Move fallbacks to the end of their switch elements + $fallbacks = $this->xpath->query( + "//text[not(@systemLanguage)]" . "|" . "//svg:text[not(@systemLanguage)]" + ); + $count = $fallbacks->length; + for ( $i = 0; $i < $count; $i++ ) { + $fallbacks->item( $i )->parentNode->appendChild( $fallbacks->item( $i ) ); + } + + return array( + 'expanded' => array_unique( $expanded ), + 'started' => array_unique( $started ) + ); } -} + + /** + * Export the SVG as a string, i.e. as "<?xml version...</svg>" + * + * @return string + */ + public function saveToString() { + // Could have simply overridden __toString() but probably not a good idea with + // no clear benefit. + return $this->document->saveXML(); + } + + /** + * Export the SVG to the desired filepath + * + * @param string $path + * @return int|bool The number of bytes written or false if an error occurred. + */ + public function saveToPath( $path ) { + return $this->document->save( $path ); + } + + /** + * Factory method for getting the related SVGFile for an SVGMessageGroup + * + * @param SVGMessageGroup $group + * @throws MWException + * @return SVGFile + * + * @todo Separate the concepts of source and fallback languages + */ + public static function newFromMessageGroup( SVGMessageGroup $group ) { + $title = Title::makeTitleSafe( NS_FILE, $group->getId() ); + $file = wfFindFile( $title ); + if ( !$file || !$file->exists() ) { + // Double-check it definitely exists + throw new MWException( 'File not found' ); + } + + return new SVGFile( $file->getLocalRefPath(), $group->getSourceLanguage() ); + } +} \ No newline at end of file diff --git a/SVGFormatWriter.php b/SVGFormatWriter.php index 795c139..0002425 100644 --- a/SVGFormatWriter.php +++ b/SVGFormatWriter.php @@ -17,14 +17,16 @@ * @var SVGMessageGroup */ protected $group; - protected $url; /** - * @var SVGFormatReader + * @var SVGFile */ - protected $reader; + protected $svg; + protected $url; protected $filename; protected $file; + + protected $inProgressTranslations = array(); /** * Constructor @@ -32,11 +34,70 @@ * @param SVGMessageGroup $group Message group to write to file * @param array $inProgressTranslations Possible array of overriddes (unsaved translations that should take preference over saved ones), format: [id][langcode][property name] */ - public function __construct( SVGMessageGroup $group, $overrides = array() ) { + public function __construct( SVGMessageGroup $group, $inProgressTranslations = array() ) { $this->group = $group; - $this->reader = new SVGFormatReader( $group, $overrides ); + $this->svg = SVGFile::newFromMessageGroup( $this->group ); + $this->inProgressTranslations = $inProgressTranslations; $this->filename = $this->group->getId(); $this->file = wfFindFile( Title::makeTitle( NS_FILE, $this->filename ) ); + } + + /** + * Get the array of in-progress translations + * @return array + */ + public function getInProgressTranslations() { + return $this->inProgressTranslations; + } + + /* + * Collate and prepare an array of translations from multiple sources: + * in file, on wiki, filteredTextNodes and in-progress. + * + * return array Array of translations + */ + protected function getPreferredTranslations() { + $inFileTranslations = $this->svg->getInFileTranslations(); + $onWikiTranslations = $this->group->getOnWikiTranslations(); + $inProgressTranslations = $this->getInProgressTranslations(); + + // Collapse in-progress translations into on-wiki translations + foreach ( $inProgressTranslations as $key => $languages ) { + foreach ( $languages as $language => $translation ) { + $language = ( $this->group->getSourceLanguage() === $language ) ? 'fallback' : $language; + $onWikiTranslations[$key][$language] = TranslateSvgUtils::translationToArray( $translation ); + } + } + + // Collapse on-wiki translations translations into in-progress translations + foreach ( $onWikiTranslations as $key => $languages ) { + foreach ( $languages as $language => $translation ) { + $oldItem = isset( $inFileTranslations[$key][$language] ) ? $inFileTranslations[$key][$language] : array(); + $inFileTranslations[$key][$language] = $onWikiTranslations[$key][$language] + $oldItem; + if ( $language !== 'fallback' ) { + $inFileTranslations[$key][$language]['id'] = $inFileTranslations[$key]['fallback']['id'] . "-$language"; + } + } + } + + // "Unfilter" translations + $inFileTranslations = array_merge( $inFileTranslations, $this->svg->getFilteredTextNodes() ); + + // Ensure that child tspan translations prompt new <text>s to be created + // by duplicating the fallback version. + foreach ( $inFileTranslations as $languages ) { + foreach ( $languages as $language => $translation ) { + if ( isset( $languages['fallback']['data-parent'] ) ) { + $parent = $languages['fallback']['data-parent']; + $inFileTranslations[$parent][$language] = $inFileTranslations[$parent]['fallback']; + if ( $language !== 'fallback' ) { + $inFileTranslations[$parent][$language]['id'] .= "-$language"; + } + } + } + } + + return $inFileTranslations; } /** @@ -59,7 +120,7 @@ $wgTranslateSvgPath = "{$wgUploadPath}/translatesvg"; } - $svg = $this->reader->getSVG(); + $this->svg->switchToTranslationSet( $this->getPreferredTranslations() ); $srcPath = tempnam( wfTempDir(), 'trans' ); $srcTempFile = new TempFSFile( $srcPath ); @@ -69,7 +130,7 @@ $intTempFile = new TempFSFile( $srcPath ); $intTempFile->autocollect(); // destroy file when $tempFsFile leaves scope - $contentsHash = substr( md5( $svg->saveXML() ), 0, 12 ); + $contentsHash = substr( md5( $this->svg->saveToString() ), 0, 12 ); $nameHash = md5( $this->filename ); $nameHashPath = substr( $nameHash, 0, 1 ) . '/' . substr( $nameHash, 0, 2 ); $dstPath = $this->getBackend()->getRootStoragePath() . @@ -86,7 +147,7 @@ } // Save the SVG to a temporary file - if ( !$svg->save( $srcPath ) ) { + if ( !$this->svg->saveToPath( $srcPath ) ) { return array( 'success' => false, 'message' => wfMessage( 'thumbnail-temp-create' )->text() ); } @@ -138,13 +199,13 @@ public function exportToSVG( User $user ) { global $wgTranslateSvgBotName, $wgContLang, $wgOut; - $svg = $this->reader->getSVG(); + $languages = $this->svg->switchToTranslationSet( $this->getPreferredTranslations() ); // Analyze what changes have been made: // * $started contains new languages; // * $expanded contains old languages with new translations - $started = $this->reader->getStarted(); - $expanded = $this->reader->getExpanded(); + $started = $languages['started']; + $expanded = $languages['expanded']; if ( count( $started ) === 0 && count( $expanded ) === 0 ) { // No real change, jump to save just a a null edit might return true; @@ -154,10 +215,10 @@ $startedString = $expandedString = wfMessage( 'translate-svg-upload-none' )->inContentLanguage(); if ( count( $started ) !== 0 ) { - $startedString = $wgContLang->commaList( array_keys( $started ) ); + $startedString = $wgContLang->commaList( $started ); } if ( count( $expanded ) !== 0 ) { - $expandedString = $wgContLang->commaList( array_keys( $expanded ) ); + $expandedString = $wgContLang->commaList( $expanded ); } $comment = wfMessage( 'translate-svg-upload-comment', @@ -166,7 +227,7 @@ // Save SVG to temp $temp = tempnam( wfTempDir(), 'trans' ); - $svg->save( $temp ); + $this->svg->saveToPath( $temp ); // Prepare upload $uploader = new TranslateSvgUpload(); diff --git a/SVGMessageGroup.php b/SVGMessageGroup.php index dda0561..2562baa 100644 --- a/SVGMessageGroup.php +++ b/SVGMessageGroup.php @@ -16,6 +16,7 @@ class SVGMessageGroup extends WikiMessageGroup { protected $source = null; protected $sourceLanguage = null; + protected $onWikiTranslations = null; /** * Constructor. @@ -145,12 +146,12 @@ global $wgTranslateSvgBotName; $bot = User::newFromName( $wgTranslateSvgBotName, false ); - $reader = new SVGFormatReader( $this ); - if ( !$reader ) { + $svg = SVGFile::newFromMessageGroup( $this ); + if ( $svg === null ) { return false; } - $translations = $reader->getInFileTranslations(); + $translations = $svg->getInFileTranslations(); foreach ( $translations as $key => $outerArray ) { foreach ( $outerArray as $language => $innerArray ) { if ( $language === 'fallback' ) { @@ -224,6 +225,49 @@ return $languages; } + /* + * Extract translations from on wiki + * + * @param bool $forceUpdate Force the regeneration the list (default: false) + * @return array Array of translations (indexed by ID, then langcode, then property) + */ + public function getOnWikiTranslations( $forceUpdate = false ) { + if( $this->onWikiTranslations !== null && !$forceUpdate ) { + return $this->onWikiTranslations; + } + + $onWikiTranslations = array(); + $languages = $this->getOnWikiLanguages(); + + // Translations generated onwiki + foreach ( $languages as $language ) { + $collection = $this->initCollection( $language ); + $collection->loadTranslations(); + $mangler = $this->getMangler(); + foreach ( $collection as $item ) { + /** @var TMessage $item */ + $key = explode( '/', $mangler->unMangle( $item->key() ) ); + $key = array_pop( $key ); + $translation = str_replace( TRANSLATE_FUZZY, '', $item->translation() ); + + if ( $translation === '' ) { + // No translation provided + continue; + } + + $language = ( $this->getSourceLanguage() == $language ) ? 'fallback' : $language; + $item = TranslateSvgUtils::translationToArray( $translation ); + if ( !isset( $onWikiTranslations[$key] ) ) { + $onWikiTranslations[$key] = array(); + } + $onWikiTranslations[$key][$language] = $item; + } + } + + $this->onWikiTranslations = $onWikiTranslations; + return $this->onWikiTranslations; + } + public function register( $useJobQueue = true ) { $articleId = Title::newFromText( $this->getLabel(), NS_FILE )->getArticleId(); diff --git a/TranslateSvg.php b/TranslateSvg.php index 8bcc020..dc7b80e 100644 --- a/TranslateSvg.php +++ b/TranslateSvg.php @@ -18,7 +18,7 @@ $dir = dirname( __FILE__ ) . '/'; $wgAutoloadClasses['SpecialTranslateNewSVG'] = $dir . 'SpecialTranslateNewSVG.php'; -$wgAutoloadClasses['SVGFormatReader'] = $dir . 'SVGFormatReader.php'; +$wgAutoloadClasses['SVGFile'] = $dir . 'SVGFile.php'; $wgAutoloadClasses['SVGFormatWriter'] = $dir . 'SVGFormatWriter.php'; $wgAutoloadClasses['SVGMessageGroup'] = $dir . 'SVGMessageGroup.php'; $wgAutoloadClasses['TranslateSvgUtils'] = $dir . 'TranslateSvgUtils.php'; diff --git a/TranslateSvgHooks.php b/TranslateSvgHooks.php index 8b17586..8deee7a 100644 --- a/TranslateSvgHooks.php +++ b/TranslateSvgHooks.php @@ -416,8 +416,8 @@ $id = $title->getText(); $messageGroup = new SVGMessageGroup( $id ); - $reader = new SVGFormatReader( $messageGroup ); - $vars['wgFileCanBeTranslated'] = ( $reader !== null ); + $svg = SVGFile::newFromMessageGroup( $messageGroup ); + $vars['wgFileCanBeTranslated'] = ( $svg->isTranslationReady() ); if ( !$vars['wgFileCanBeTranslated'] || MessageGroups::getGroup( $id ) === null ) { // Not translatable or not yet translated, let's save time and return immediately $vars['wgFileTranslationStarted'] = false; @@ -426,7 +426,7 @@ return true; } - $languages = $reader->getSavedLanguagesFiltered(); + $languages = $svg->getSavedLanguagesFiltered(); $full = array(); $partial = array(); foreach ( $languages['full'] as $language ) { diff --git a/TranslateSvgTasks.php b/TranslateSvgTasks.php index 1349f4e..8e91fcc 100644 --- a/TranslateSvgTasks.php +++ b/TranslateSvgTasks.php @@ -25,8 +25,7 @@ return $this->errorOutput( wfMessage( 'translate-svg-export-unsupported', $link ) ); } - /** @var SVGFormatWriter $writer */ - $writer = $this->group->getWriter(); + $writer = new SVGFormatWriter( $this->group ); $ret = $writer->exportToSVG( $this->context->getUser() ); if ( $ret === true ) { global $wgOut; diff --git a/tests/phpunit/SVGFormatReaderTest.php b/tests/phpunit/SVGFileTest.php similarity index 89% rename from tests/phpunit/SVGFormatReaderTest.php rename to tests/phpunit/SVGFileTest.php index 76e8e96..2e7de27 100644 --- a/tests/phpunit/SVGFormatReaderTest.php +++ b/tests/phpunit/SVGFileTest.php @@ -10,24 +10,19 @@ */ /** - * Unit tests for SVGFormatReader class. - * @covers SVGFormatReader + * Unit tests for SVGFile class. + * @covers SVGFile */ -class SVGFormatReaderTest extends TranslateSvgTestCase { +class SVGFileTest extends TranslateSvgTestCase { /** - * @var SVGFormatReader + * @var SVGFile */ - private $reader; - - public static function setUpBeforeClass() { - parent::setUpBeforeClass(); - self::prepareFile( __DIR__ . '/../data/Speech_bubbles.svg' ); - } + private $svg; public function setUp() { parent::setUp(); - $this->reader = new SVGFormatReader( $this->messageGroup ); + $this->svg = new SVGFile( __DIR__ . '/../data/Speech_bubbles.svg', 'en' ); } public function testGetInFileTranslations() { @@ -238,14 +233,14 @@ ), ) ); - $this->assertEquals( $expected, $this->reader->getInFileTranslations() ); + $this->assertEquals( $expected, $this->svg->getInFileTranslations() ); } public function testGetSavedLanguages() { $expected = array( 'de', 'fr', 'nl', 'tlh-ca', 'en' ); - $this->assertEquals( $expected, $this->reader->getSavedLanguages() ); + $this->assertEquals( $expected, $this->svg->getSavedLanguages() ); } public function testGetSavedLanguagesFiltered() { @@ -253,7 +248,7 @@ 'full' => array( 'fr', 'nl', 'tlh-ca', 'en' ), 'partial' => array( 'de' ) ); - $this->assertEquals( $expected, $this->reader->getSavedLanguagesFiltered() ); + $this->assertEquals( $expected, $this->svg->getSavedLanguagesFiltered() ); } } \ No newline at end of file -- To view, visit https://gerrit.wikimedia.org/r/156129 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ia2fc5cb8e07787c6039e1c9520a7b6f33284f3a4 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/TranslateSvg Gerrit-Branch: master Gerrit-Owner: Jarry1250 <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
