Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
BackportTranslationsMaintenanceScript.php
1<?php
2declare( strict_types = 1 );
3
5
13use MediaWiki\Logger\LoggerFactory;
14use RuntimeException;
15
28 private const CHANGED_CODES = [ 'cdo', 'nan', 'sh', 'wuu', 'yue' ];
29
30 public function __construct() {
31 parent::__construct();
32 $this->addDescription( 'Backport translations from one branch to another.' );
33
34 $this->addOption(
35 'group',
36 'Comma separated list of message group IDs (supports * wildcard) to backport',
37 self::REQUIRED,
38 self::HAS_ARG
39 );
40 $this->addOption(
41 'source-path',
42 'Source path for reading updated translations. Defaults to $wgTranslateGroupRoot.',
43 self::OPTIONAL,
44 self::HAS_ARG
45 );
46 $this->addOption(
47 'target-path',
48 'Target path for writing backported translations',
49 self::REQUIRED,
50 self::HAS_ARG
51 );
52 $this->addOption(
53 'filter-path',
54 'Only export a group if its export path matches this prefix (relative to target-path)',
55 self::OPTIONAL,
56 self::HAS_ARG
57 );
58 $this->addOption(
59 'never-export-languages',
60 'Languages to not export',
61 self::OPTIONAL,
62 self::HAS_ARG
63 );
64 $this->requireExtension( 'Translate' );
65 }
66
68 public function execute() {
69 $config = $this->getConfig();
70 $logger = LoggerFactory::getInstance( LogNames::GROUP_SYNCHRONIZATION );
71 $groupPattern = $this->getOption( 'group' ) ?? '';
72 $logger->info(
73 'Starting backports for groups {groups}',
74 [ 'groups' => $groupPattern ]
75 );
76
77 $sourcePath = $this->getOption( 'source-path' ) ?: $config->get( 'TranslateGroupRoot' );
78 if ( !is_readable( $sourcePath ) ) {
79 $this->fatalError( "Source directory is not readable ($sourcePath)." );
80 }
81
82 $targetPath = $this->getOption( 'target-path' );
83 if ( !is_writable( $targetPath ) ) {
84 $this->fatalError( "Target directory is not writable ($targetPath)." );
85 }
86
87 $groupIds = MessageGroups::expandWildcards( explode( ',', trim( $groupPattern ) ) );
88 $groups = MessageGroups::getGroupsById( $groupIds );
89 if ( $groups === [] ) {
90 $this->fatalError( "Pattern $groupPattern did not match any message groups." );
91 }
92
93 $neverExportLanguages = $this->csv2array(
94 $this->getOption( 'never-export-languages' ) ?? ''
95 );
96 $supportedLanguages = Utilities::getLanguageNames( 'en' );
97
98 foreach ( $groups as $group ) {
99 $groupId = $group->getId();
100 if ( !$group instanceof FileBasedMessageGroup ) {
101 if ( !$this->hasOption( 'filter-path' ) ) {
102 $this->error( "Skipping $groupId: Not instance of FileBasedMessageGroup" );
103 }
104 continue;
105 }
106
107 if ( !$group->getFFS() instanceof JsonFormat ) {
108 $this->error( "Skipping $groupId: Only JSON format is supported" );
109 continue;
110 }
111
112 if ( $this->hasOption( 'filter-path' ) ) {
113 $filter = $this->getOption( 'filter-path' );
114 $exportPath = $group->getTargetFilename( '*' );
115 if ( !$this->matchPath( $filter, $exportPath ) ) {
116 continue;
117 }
118 }
119
120 $sourceLanguage = $group->getSourceLanguage();
121 try {
122 $sourceDefinitions = $this->loadDefinitions( $group, $sourcePath, $sourceLanguage );
123 $targetDefinitions = $this->loadDefinitions( $group, $targetPath, $sourceLanguage );
124 } catch ( RuntimeException $e ) {
125 $this->output(
126 "Skipping $groupId: Error while loading definitions: {$e->getMessage()}\n"
127 );
128 continue;
129 }
130
131 $keyCompatibilityMap = $this->getKeyCompatibilityMap(
132 $sourceDefinitions['MESSAGES'],
133 $targetDefinitions['MESSAGES'],
134 $group->getFFS()
135 );
136
137 if ( array_filter( $keyCompatibilityMap ) === [] ) {
138 $this->output( "Skipping $groupId: No compatible keys found\n" );
139 continue;
140 }
141
142 $summary = [];
143 $languages = array_keys( $group->getTranslatableLanguages() ?? $supportedLanguages );
144 $languagesToSkip = $neverExportLanguages;
145 $languagesToSkip[] = $sourceLanguage;
146 $languages = array_diff( $languages, $languagesToSkip );
147
148 foreach ( $languages as $language ) {
149 $status = $this->backport(
150 $group,
151 $sourcePath,
152 $targetPath,
153 $keyCompatibilityMap,
154 $language
155 );
156
157 $summary[$status][] = $language;
158 }
159
160 $numUpdated = count( $summary[ 'updated' ] ?? [] );
161 $numAdded = count( $summary[ 'new' ] ?? [] );
162 if ( ( $numUpdated + $numAdded ) > 0 ) {
163 $this->output(
164 sprintf(
165 "%s: Compatible keys: %d. Updated %d languages, %d new (%s)\n",
166 $group->getId(),
167 count( $keyCompatibilityMap ),
168 $numUpdated,
169 $numAdded,
170 implode( ', ', $summary[ 'new' ] ?? [] )
171 )
172 );
173 }
174 }
175 }
176
177 private function csv2array( string $input ): array {
178 return array_filter(
179 array_map( 'trim', explode( ',', $input ) ),
180 static function ( $v ) {
181 return $v !== '';
182 }
183 );
184 }
185
186 private function matchPath( string $prefix, string $full ): bool {
187 $prefix = "./$prefix";
188 $length = strlen( $prefix );
189 return substr( $full, 0, $length ) === $prefix;
190 }
191
192 private function loadDefinitions(
194 string $path,
195 string $language
196 ): array {
197 $file = $path . '/' . $group->getTargetFilename( $language );
198
199 if ( !file_exists( $file ) ) {
200 throw new RuntimeException( "File $file does not exist" );
201 }
202
203 $contents = file_get_contents( $file );
204 return $group->getFFS()->readFromVariable( $contents );
205 }
206
216 private function getKeyCompatibilityMap( array $source, array $target, SimpleFormat $fileFormat ): array {
217 $keys = [];
218 foreach ( $target as $key => $value ) {
219 $keys[$key] = isset( $source[ $key ] ) && $fileFormat->isContentEqual( $source[ $key ], $value );
220 }
221 return $keys;
222 }
223
224 private function backport(
226 string $source,
227 string $targetPath,
228 array $keyCompatibilityMap,
229 string $language
230 ): string {
231 try {
232 $sourceTemplate = $this->loadDefinitions( $group, $source, $language );
233 } catch ( RuntimeException $e ) {
234 return 'no definitions';
235 }
236
237 try {
238 $targetTemplate = $this->loadDefinitions( $group, $targetPath, $language );
239 } catch ( RuntimeException $e ) {
240 $targetTemplate = [
241 'MESSAGES' => [],
242 'AUTHORS' => [],
243 ];
244 }
245
246 // Amend the target with compatible things from the source
247 $hasUpdates = false;
248
249 $fileFormat = $group->getFFS();
250
251 // This has been checked before, but checking again to keep Phan and IDEs happy.
252 // Remove once support for other file formats are added.
253 if ( !$fileFormat instanceof JsonFormat ) {
254 throw new RuntimeException(
255 "Expected file format type: " . JsonFormat::class . '; got: ' . get_class( $fileFormat )
256 );
257 }
258
259 $combinedMessages = [];
260 // $keyCompatibilityMap has the target (stable branch) source language key order
261 foreach ( $keyCompatibilityMap as $key => $isDefinitionCompatible ) {
262 $sourceValue = $sourceTemplate['MESSAGES'][$key] ?? null;
263 $targetValue = $targetTemplate['MESSAGES'][$key] ?? null;
264
265 // Use existing translation value from the target (stable branch) as the default
266 if ( $targetValue !== null ) {
267 $combinedMessages[$key] = $targetValue;
268 }
269
270 // The source (development branch) message definition in the source language is not compatible with the
271 // target (stable branch) definition. Skip exporting the message
272 if ( !$isDefinitionCompatible ) {
273 continue;
274 }
275
276 if ( $sourceValue === null ) {
277 if ( in_array( $language, self::CHANGED_CODES, true ) ) {
278 continue;
279 }
280 // Translation no longer exists, remove it from the stable branch. See: T375487
281 $hasUpdates = true;
282 unset( $combinedMessages[$key] );
283 } elseif ( !$fileFormat->isContentEqual( $sourceValue, $targetValue ) ) {
284 // {{#FORMAL:}} magic word was introduced in 1.43. Temporary skip backporting translations using it.
285 if ( str_contains( $sourceValue, '#FORMAL:' ) ) {
286 continue;
287 }
288
289 // Keep track if we actually overwrote any values, so we can report back stats
290 $hasUpdates = true;
291 $combinedMessages[$key] = $sourceValue;
292 }
293 }
294
295 if ( !$hasUpdates ) {
296 return 'no updates';
297 }
298
299 // Copy over all authors (we do not know per-message level)
300 $combinedAuthors = array_merge(
301 $targetTemplate[ 'AUTHORS' ] ?? [],
302 $sourceTemplate[ 'AUTHORS' ] ?? []
303 );
304 $combinedAuthors = array_unique( $combinedAuthors );
305 $combinedAuthors = $fileFormat->filterAuthors( $combinedAuthors, $language );
306
307 $targetTemplate['AUTHORS'] = $combinedAuthors;
308 $targetTemplate['MESSAGES'] = $combinedMessages;
309
310 $backportedContent = $fileFormat->generateFile( $targetTemplate );
311
312 $targetFilename = $targetPath . '/' . $group->getTargetFilename( $language );
313 if ( file_exists( $targetFilename ) ) {
314 $currentContent = file_get_contents( $targetFilename );
315
316 if ( $fileFormat->shouldOverwrite( $currentContent, $backportedContent ) ) {
317 file_put_contents( $targetFilename, $backportedContent );
318 }
319 return 'updated';
320 } else {
321 file_put_contents( $targetFilename, $backportedContent );
322 return 'new';
323 }
324 }
325}
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(), 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: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 { return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, '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());}, '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->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.
JsonFormat implements a message format where messages are encoded as key-value pairs in JSON objects.
A very basic FileFormatSupport module that implements some basic functionality and a simple binary ba...
Constants for log channel names used in this extension.
Definition LogNames.php:13
const GROUP_SYNCHRONIZATION
Channel for message group synchronization.
Definition LogNames.php:18
Factory class for accessing message groups individually by id or all of them as a list.
Base maintenance script containing constants and methods used in multiple scripts Hopefully the const...
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31
Finds external changes for file based message groups.