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 UtfNormal\Validator;
15use Wikimedia\StringUtils\StringUtils;
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 // Warning: this exception message contains a local filesystem path. If this method
179 // is ever called from a web-facing context, the message must be sanitized to avoid
180 // disclosing server directory structure to end users.
181 throw new InvalidArgumentException( "Write path '$writePath' does not exist." );
182 }
183
184 if ( !is_writable( $writePath ) ) {
185 // Warning: this exception message contains a local filesystem path (see above).
186 throw new InvalidArgumentException( "Write path '$writePath' is not writable." );
187 }
188
189 $targetFile = $writePath . '/' . $this->group->getTargetFilename( $collection->code );
190
191 $targetFileExists = file_exists( $targetFile );
192
193 if ( $targetFileExists ) {
194 $this->tryReadSource( $targetFile, $collection );
195 } else {
196 $sourceFile = $this->group->getSourceFilePath( $collection->code );
197 $this->tryReadSource( $sourceFile, $collection );
198 }
199
200 $output = $this->writeReal( $collection );
201 if ( !$output ) {
202 return;
203 }
204
205 // Some file formats might have changing parts, such as timestamp.
206 // This allows the file handler to skip updating files, where only
207 // the timestamp would change.
208 if ( $targetFileExists ) {
209 $oldContent = $this->tryReadFile( $targetFile );
210 if ( $oldContent === null || !$this->shouldOverwrite( $oldContent, $output ) ) {
211 return;
212 }
213 }
214
215 wfMkdirParents( dirname( $targetFile ), null, __METHOD__ );
216 file_put_contents( $targetFile, $output );
217 }
218
220 public function writeIntoVariable( MessageCollection $collection ): string {
221 $sourceFile = $this->group->getSourceFilePath( $collection->code );
222 $this->tryReadSource( $sourceFile, $collection );
223
224 return $this->writeReal( $collection );
225 }
226
227 protected function writeReal( MessageCollection $collection ): string {
228 $output = '';
229
230 $authors = $collection->getAuthors();
231 $authors = $this->filterAuthors( $authors, $collection->code );
232
233 $output .= implode( self::RECORD_SEPARATOR, $authors );
234 $output .= self::PART_SEPARATOR;
235
236 $mangler = $this->group->getMangler();
237
239 foreach ( $collection as $key => $m ) {
240 $key = $mangler->unmangle( $key );
241 $trans = $m->translation();
242 $output .= "$key=$trans" . self::RECORD_SEPARATOR;
243 }
244
245 return $output;
246 }
247
255 protected function tryReadSource( string $filename, MessageCollection $collection ): void {
256 if ( !$this->isGroupFfsReadable() ) {
257 return;
258 }
259
260 $sourceText = $this->tryReadFile( $filename );
261
262 // No need to do anything in SimpleFormat if it's null,
263 // it only reads author data from it.
264 if ( $sourceText !== null ) {
265 $sourceData = $this->readFromVariable( $sourceText );
266
267 if ( isset( $sourceData['AUTHORS'] ) ) {
268 $collection->addCollectionAuthors( $sourceData['AUTHORS'] );
269 }
270 }
271 }
272
280 protected function tryReadFile( string $filename ): ?string {
281 if ( $filename === '' || !file_exists( $filename ) ) {
282 return null;
283 }
284
285 if ( !is_readable( $filename ) ) {
286 throw new InvalidArgumentException( "File $filename is not readable." );
287 }
288
289 $data = file_get_contents( $filename );
290 if ( $data === false ) {
291 throw new InvalidArgumentException( "Unable to read file $filename." );
292 }
293
294 return $data;
295 }
296
298 public function filterAuthors( array $authors, string $code ): array {
299 $configHelper = Services::getInstance()->getConfigHelper();
300 foreach ( $authors as $i => $v ) {
301 if ( $configHelper->isAuthorExcluded( $this->group, $code, (string)$v ) ) {
302 unset( $authors[$i] );
303 }
304 }
305
306 return array_values( $authors );
307 }
308
309 public function isContentEqual( ?string $a, ?string $b ): bool {
310 return $a === $b;
311 }
312
313 public function shouldOverwrite( string $a, string $b ): bool {
314 return true;
315 }
316
322 public function isGroupFfsReadable(): bool {
323 try {
324 $ffs = $this->group->getFFS();
325 } catch ( RuntimeException $e ) {
326 if ( $e->getCode() === FileBasedMessageGroup::NO_FILE_FORMAT ) {
327 return false;
328 }
329
330 throw $e;
331 }
332
333 return get_class( $ffs ) === get_class( $this );
334 }
335}
336
337class_alias( SimpleFormat::class, 'SimpleFFS' );
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager($services->getTitleFactory(), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, '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:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance(LogNames::GROUP_SYNCHRONIZATION), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), $services->getContentLanguageCode() ->toString(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleDependencyPurger'=> static function(MediaWikiServices $services):MessageBundleDependencyPurger { return new MessageBundleDependencyPurger( $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupFactory'=> static function(MediaWikiServices $services):MessageGroupFactory { return new MessageGroupFactory($services->get( 'Translate:MessageGroupTypeRegistry'));}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getConnectionProvider());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getConnectionProvider(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):?MessageGroupSubscriptionHookHandler { if(! $services->getExtensionRegistry() ->isLoaded( 'Echo')) { return null;} return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageGroupTypeRegistry'=> static function():MessageGroupTypeRegistry { return new MessageGroupTypeRegistry();}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance(LogNames::MAIN), $services->getMainObjectStash(), $services->getConnectionProvider(), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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->getConnectionProvider(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getConnectionProvider());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getConnectionProvider());}, '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(), $services->getNamespaceInfo(), $services->getTitleFactory(), $services->getFormatterFactory());}, '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->getConnectionProvider(), $services->getObjectCacheFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getConnectionProvider() ->getPrimaryDatabase(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getConnectionProvider(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'), $services->get( 'Translate:MessageGroupSubscription'), $services->getFormatterFactory(), $services->get( 'Translate:HookRunner'),);}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getConnectionProvider(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getConnectionProvider(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getConnectionProvider(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { return new TranslationStashStorage( $services->getConnectionProvider() ->getPrimaryDatabase());}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getConnectionProvider());}, '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->getConnectionProvider());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);}, 'Translate:WorkflowStatesMessageGroupLoader'=> static function(MediaWikiServices $services):WorkflowStatesMessageGroupLoader { return new WorkflowStatesMessageGroupLoader(new ServiceOptions(WorkflowStatesMessageGroupLoader::CONSTRUCTOR_OPTIONS, $services->getMainConfig()),);},]
@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:62