Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
SimpleFormat.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\FileFormatSupport;
5
6use Exception;
8use InvalidArgumentException;
9use LogicException;
13use RuntimeException;
14use StringUtils;
15use UtfNormal\Validator;
16
25
26 public function supportsFuzzy(): string {
27 return 'no';
28 }
29
30 public function getFileExtensions(): array {
31 return [];
32 }
33
34 protected FileBasedMessageGroup $group;
35 protected ?string $writePath = null;
41 protected $extra;
42
43 private const RECORD_SEPARATOR = "\0";
44 private const PART_SEPARATOR = "\0\0\0\0";
45
46 public function __construct( FileBasedMessageGroup $group ) {
47 $this->setGroup( $group );
48 $conf = $group->getConfiguration();
49 $this->extra = $conf['FILES'];
50 }
51
52 public function setGroup( FileBasedMessageGroup $group ): void {
53 $this->group = $group;
54 }
55
56 public function getGroup(): FileBasedMessageGroup {
57 return $this->group;
58 }
59
60 public function setWritePath( string $target ): void {
61 $this->writePath = $target;
62 }
63
64 public function getWritePath(): string {
65 return $this->writePath;
66 }
67
77 public function exists( $code = false ): bool {
78 if ( $code === false ) {
79 $code = $this->group->getSourceLanguage();
80 }
81
82 $filename = $this->group->getSourceFilePath( $code );
83 if ( $filename === null ) {
84 return false;
85 }
86
87 return file_exists( $filename );
88 }
89
97 public function read( string $languageCode ) {
98 if ( !$this->isGroupFfsReadable() ) {
99 return [];
100 }
101
102 if ( !$this->exists( $languageCode ) ) {
103 return false;
104 }
105
106 $filename = $this->group->getSourceFilePath( $languageCode );
107 $input = file_get_contents( $filename );
108 if ( $input === false ) {
109 throw new RuntimeException( "Unable to read file $filename." );
110 }
111
112 if ( !StringUtils::isUtf8( $input ) ) {
113 throw new RuntimeException( "Contents of $filename are not valid utf-8." );
114 }
115
116 $input = Validator::cleanUp( $input );
117
118 // Strip BOM mark
119 $input = ltrim( $input, "\u{FEFF}" );
120
121 try {
122 return $this->readFromVariable( $input );
123 } catch ( Exception $e ) {
124 throw new RuntimeException( "Parsing $filename failed: " . $e->getMessage() );
125 }
126 }
127
134 public function readFromVariable( string $data ): array {
135 $parts = explode( self::PART_SEPARATOR, $data );
136
137 if ( count( $parts ) !== 2 ) {
138 throw new InvalidArgumentException( 'Wrong number of parts.' );
139 }
140
141 [ $authorsPart, $messagesPart ] = $parts;
142 $authors = explode( self::RECORD_SEPARATOR, $authorsPart );
143 $messages = [];
144
145 foreach ( explode( self::RECORD_SEPARATOR, $messagesPart ) as $line ) {
146 if ( $line === '' ) {
147 continue;
148 }
149
150 $lineParts = explode( '=', $line, 2 );
151
152 if ( count( $lineParts ) !== 2 ) {
153 throw new InvalidArgumentException( "Wrong number of parts in line $line." );
154 }
155
156 [ $key, $message ] = $lineParts;
157 $key = trim( $key );
158 $messages[$key] = $message;
159 }
160
161 $messages = $this->group->getMangler()->mangleArray( $messages );
162
163 return [
164 'AUTHORS' => $authors,
165 'MESSAGES' => $messages,
166 ];
167 }
168
170 public function write( MessageCollection $collection ): void {
171 $writePath = $this->writePath;
172
173 if ( $writePath === null ) {
174 throw new LogicException( 'Write path is not set. Set write path before calling write()' );
175 }
176
177 if ( !file_exists( $writePath ) ) {
178 throw new InvalidArgumentException( "Write path '$writePath' does not exist." );
179 }
180
181 if ( !is_writable( $writePath ) ) {
182 throw new InvalidArgumentException( "Write path '$writePath' is not writable." );
183 }
184
185 $targetFile = $writePath . '/' . $this->group->getTargetFilename( $collection->code );
186
187 $targetFileExists = file_exists( $targetFile );
188
189 if ( $targetFileExists ) {
190 $this->tryReadSource( $targetFile, $collection );
191 } else {
192 $sourceFile = $this->group->getSourceFilePath( $collection->code );
193 $this->tryReadSource( $sourceFile, $collection );
194 }
195
196 $output = $this->writeReal( $collection );
197 if ( !$output ) {
198 return;
199 }
200
201 // Some file formats might have changing parts, such as timestamp.
202 // This allows the file handler to skip updating files, where only
203 // the timestamp would change.
204 if ( $targetFileExists ) {
205 $oldContent = $this->tryReadFile( $targetFile );
206 if ( $oldContent === null || !$this->shouldOverwrite( $oldContent, $output ) ) {
207 return;
208 }
209 }
210
211 wfMkdirParents( dirname( $targetFile ), null, __METHOD__ );
212 file_put_contents( $targetFile, $output );
213 }
214
216 public function writeIntoVariable( MessageCollection $collection ): string {
217 $sourceFile = $this->group->getSourceFilePath( $collection->code );
218 $this->tryReadSource( $sourceFile, $collection );
219
220 return $this->writeReal( $collection );
221 }
222
223 protected function writeReal( MessageCollection $collection ): string {
224 $output = '';
225
226 $authors = $collection->getAuthors();
227 $authors = $this->filterAuthors( $authors, $collection->code );
228
229 $output .= implode( self::RECORD_SEPARATOR, $authors );
230 $output .= self::PART_SEPARATOR;
231
232 $mangler = $this->group->getMangler();
233
235 foreach ( $collection as $key => $m ) {
236 $key = $mangler->unmangle( $key );
237 $trans = $m->translation();
238 $output .= "$key=$trans" . self::RECORD_SEPARATOR;
239 }
240
241 return $output;
242 }
243
251 protected function tryReadSource( string $filename, MessageCollection $collection ): void {
252 if ( !$this->isGroupFfsReadable() ) {
253 return;
254 }
255
256 $sourceText = $this->tryReadFile( $filename );
257
258 // No need to do anything in SimpleFormat if it's null,
259 // it only reads author data from it.
260 if ( $sourceText !== null ) {
261 $sourceData = $this->readFromVariable( $sourceText );
262
263 if ( isset( $sourceData['AUTHORS'] ) ) {
264 $collection->addCollectionAuthors( $sourceData['AUTHORS'] );
265 }
266 }
267 }
268
276 protected function tryReadFile( string $filename ): ?string {
277 if ( $filename === '' || !file_exists( $filename ) ) {
278 return null;
279 }
280
281 if ( !is_readable( $filename ) ) {
282 throw new InvalidArgumentException( "File $filename is not readable." );
283 }
284
285 $data = file_get_contents( $filename );
286 if ( $data === false ) {
287 throw new InvalidArgumentException( "Unable to read file $filename." );
288 }
289
290 return $data;
291 }
292
294 public function filterAuthors( array $authors, string $code ): array {
295 $groupId = $this->group->getId();
296 $configHelper = Services::getInstance()->getConfigHelper();
297 foreach ( $authors as $i => $v ) {
298 if ( $configHelper->isAuthorExcluded( $groupId, $code, (string)$v ) ) {
299 unset( $authors[$i] );
300 }
301 }
302
303 return array_values( $authors );
304 }
305
306 public function isContentEqual( ?string $a, ?string $b ): bool {
307 return $a === $b;
308 }
309
310 public function shouldOverwrite( string $a, string $b ): bool {
311 return true;
312 }
313
319 public function isGroupFfsReadable(): bool {
320 try {
321 $ffs = $this->group->getFFS();
322 } catch ( RuntimeException $e ) {
323 if ( $e->getCode() === FileBasedMessageGroup::NO_FILE_FORMAT ) {
324 return false;
325 }
326
327 throw $e;
328 }
329
330 return get_class( $ffs ) === get_class( $this );
331 }
332}
333
334class_alias( SimpleFormat::class, 'SimpleFFS' );
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
This class implements default behavior for file based message groups.
A very basic FileFormatSupport module that implements some basic functionality and a simple binary ba...
shouldOverwrite(string $a, string $b)
Allows to skip writing the export output into a file.
tryReadFile(string $filename)
Read the contents of $filename and return it as a string.
getWritePath()
Get the file's location in the system.
isContentEqual(?string $a, ?string $b)
Checks whether two strings are equal.
setWritePath(string $target)
Set the file's location in the system.
filterAuthors(array $authors, string $code)
Remove excluded authors.
read(string $languageCode)
Reads messages from the file in a given language and returns an array of AUTHORS, MESSAGES and possib...
getFileExtensions()
Return the commonly used file extensions for these formats.
tryReadSource(string $filename, MessageCollection $collection)
This tries to pick up external authors in the source files so that they are not lost if those authors...
write(MessageCollection $collection)
Write the collection to file.
readFromVariable(string $data)
Parse the message data given as a string in the SimpleFormat format and return it as an array of AUTH...
exists( $code=false)
Returns true if the file for this message group in a given language exists.
writeIntoVariable(MessageCollection $collection)
Read a collection and return it as a SimpleFormat formatted string.
isGroupFfsReadable()
Check if the file format of the current group is readable by the file format system.
This file contains the class for core message collections implementation.
getAuthors()
Lists all translators that have contributed to the latest revisions of each translation.
addCollectionAuthors(array $authors, string $mode='append')
Add external authors (usually from the file).
Interface for message objects used by MessageCollection.
Definition Message.php:13
Minimal service container.
Definition Services.php:44