1<?php 2/** 3 * @file 4 * @author Niklas Laxström 5 * @license GPL-2.0-or-later 6 */ 7 8namespace LocalisationUpdate; 9 10use LocalisationUpdate\Fetcher\FetcherFactory; 11use LocalisationUpdate\Reader\ReaderFactory; 12 13/** 14 * Executes the localisation update. 15 */ 16class Updater { 17 18 /** 19 * @var Update 20 */ 21 private $logger; 22 23 /** 24 * Whether the path is a pattern and thus we need to use appropriate 25 * code for fetching directories. 26 * 27 * @param string $path Url 28 * @return bool 29 */ 30 public function isDirectory( $path ) { 31 $filename = basename( $path ); 32 return strpos( $filename, '*' ) !== false; 33 } 34 35 /** 36 * Expands repository relative path to full url with the given repository 37 * patterns. Extra variables in $info are used as variables and will be 38 * replaced the pattern. 39 * 40 * @param array $info Component information. 41 * @param array $repos Repository information. 42 * @return string 43 */ 44 public function expandRemotePath( $info, $repos ) { 45 $pattern = $repos[$info['repo']]; 46 unset( $info['repo'], $info['orig'] ); 47 48 // This assumes all other keys are used as variables 49 // in the pattern. For example name -> %NAME%. 50 $keys = []; 51 foreach ( array_keys( $info ) as $key ) { 52 $keys[] = '%' . strtoupper( $key ) . '%'; 53 } 54 55 $values = array_values( $info ); 56 return str_replace( $keys, $values, $pattern ); 57 } 58 59 /** 60 * Parses translations from given list of files. 61 * 62 * @param ReaderFactory $readerFactory Factory to construct parsers. 63 * @param array $files List of files with their contents as array values. 64 * @return array List of translations indexed by language code. 65 */ 66 public function readMessages( ReaderFactory $readerFactory, array $files ) { 67 $messages = []; 68 69 foreach ( $files as $filename => $contents ) { 70 $reader = $readerFactory->getReader( $filename ); 71 try { 72 $parsed = $reader->parse( $contents ); 73 } catch ( \Exception $e ) { 74 trigger_error( __METHOD__ . ": Unable to parse messages from $filename", E_USER_WARNING ); 75 continue; 76 } 77 78 foreach ( $parsed as $code => $langMessages ) { 79 if ( !isset( $messages[$code] ) ) { 80 $messages[$code] = []; 81 } 82 $messages[$code] = array_merge( $messages[$code], $langMessages ); 83 } 84 85 $c = array_sum( array_map( 'count', $parsed ) ); 86 // Useful for debugging, maybe create interface to pass this to the script? 87 # echo "$filename with " . get_class( $reader ) . " and $c\n"; 88 } 89 90 return $messages; 91 } 92 93 /** 94 * Find new and changed translations in $remote and returns them. 95 * 96 * @param array $origin 97 * @param array $remote 98 * @param array $ignore Array of message keys to ignore, keys as as array keys. 99 * @return array 100 */ 101 public function findChangedTranslations( $origin, $remote, $ignore = [] ) { 102 $changed = []; 103 foreach ( $remote as $key => $value ) { 104 if ( isset( $ignore[$key] ) ) { 105 continue; 106 } 107 108 if ( !isset( $origin[$key] ) || $value !== $origin[$key] ) { 109 $changed[$key] = $value; 110 } 111 } 112 return $changed; 113 } 114 115 /** 116 * Fetches files from given Url pattern. 117 * 118 * @param FetcherFactory $factory Factory to construct fetchers. 119 * @param string $path Url to the file or pattern of files. 120 * @return array List of Urls with file contents as path. 121 */ 122 public function fetchFiles( FetcherFactory $factory, $path ) { 123 $fetcher = $factory->getFetcher( $path ); 124 125 if ( $this->isDirectory( $path ) ) { 126 $files = $fetcher->fetchDirectory( $path ); 127 } else { 128 $files = [ $path => $fetcher->fetchFile( $path ) ]; 129 } 130 131 // Remove files which were not found 132 return array_filter( $files ); 133 } 134 135 /** 136 * @param Finder $finder 137 * @param ReaderFactory $readerFactory 138 * @param FetcherFactory $fetcherFactory 139 * @param array $repos 140 * @param Update $logger 141 * @return array 142 */ 143 public function execute( 144 Finder $finder, 145 ReaderFactory $readerFactory, 146 FetcherFactory $fetcherFactory, 147 array $repos, 148 $logger 149 ) { 150 $components = $finder->getComponents(); 151 152 $updatedMessages = []; 153 154 foreach ( $components as $key => $info ) { 155 $logger->logInfo( "Updating component $key" ); 156 157 $originFiles = $this->fetchFiles( $fetcherFactory, $info['orig'] ); 158 $remotePath = $this->expandRemotePath( $info, $repos ); 159 try { 160 $remoteFiles = $this->fetchFiles( $fetcherFactory, $remotePath ); 161 } catch ( \Exception $e ) { 162 $logger->logError( __METHOD__ . ": Unable to fetch messages from $remotePath" ); 163 continue; 164 } 165 166 if ( $remoteFiles === [] ) { 167 // Small optimization: if nothing to compare with, skip 168 continue; 169 } 170 171 $originMessages = $this->readMessages( $readerFactory, $originFiles ); 172 $remoteMessages = $this->readMessages( $readerFactory, $remoteFiles ); 173 174 if ( !isset( $remoteMessages['en'] ) ) { 175 // Could not find remote messages 176 continue; 177 } 178 179 // If remote translation in English is not present or differs, we do not want 180 // translations for other languages for those messages, as they are either not 181 // used in this version of code or can be incompatible. 182 $forbiddenKeys = $this->findChangedTranslations( 183 $originMessages['en'], 184 $remoteMessages['en'] 185 ); 186 187 // We never accept updates for English strings 188 unset( $originMessages['en'], $remoteMessages['en'] ); 189 190 // message: string in all languages; translation: string in one language. 191 foreach ( $remoteMessages as $language => $remoteTranslations ) { 192 // Check for completely new languages 193 $originTranslations = []; 194 if ( isset( $originMessages[$language] ) ) { 195 $originTranslations = $originMessages[$language]; 196 } 197 198 $updatedTranslations = $this->findChangedTranslations( 199 $originTranslations, 200 $remoteTranslations, 201 $forbiddenKeys 202 ); 203 204 // Avoid empty arrays 205 if ( $updatedTranslations === [] ) { 206 continue; 207 } 208 209 if ( !isset( $updatedMessages[$language] ) ) { 210 $updatedMessages[$language] = []; 211 } 212 213 // In case of conflicts, which should not exist, this prefers the 214 // first translation seen. 215 $updatedMessages[$language] += $updatedTranslations; 216 } 217 } 218 219 return $updatedMessages; 220 } 221} 222