Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ImportTranslatableBundleMaintenanceScript.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use MediaWiki\Context\RequestContext;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Title\ForeignTitle;
14use MediaWiki\Title\MalformedTitleException;
15use MediaWiki\Title\Title;
16use MediaWiki\User\UserIdentity;
17use Wikimedia\Rdbms\IDBAccessObject;
18
26 private int $pageImportCount = 0;
27 private int $totalPagesBeingImported = 0;
28
29 public function __construct() {
30 parent::__construct();
31 $this->addArg(
32 'xml-path',
33 'Path to the XML file to be imported',
34 self::REQUIRED
35 );
36 $this->addOption(
37 'user',
38 'Name of the user performing the import',
39 self::REQUIRED,
40 self::HAS_ARG
41 );
42 $this->addOption(
43 'interwiki-prefix',
44 'Prefix to apply to unknown (and possibly also known) usernames',
45 self::REQUIRED,
46 self::HAS_ARG
47 );
48 $this->addOption(
49 'comment',
50 'Comment added to the log for the import',
51 self::OPTIONAL,
52 self::HAS_ARG
53 );
54 $this->addOption(
55 'assign-known-users',
56 'Whether to apply the prefix to usernames that exist locally',
57 self::OPTIONAL
58 );
59 $this->addOption(
60 'target-name',
61 'Target page name to import the page to',
62 self::OPTIONAL,
63 self::HAS_ARG
64 );
65 $this->addOption(
66 'override',
67 'Override existing target page if it exists',
68 self::OPTIONAL
69 );
70
71 // Options related to marking a page for translation
72 $this->addOption(
73 'skip-translating-title',
74 'Should translation of title be skipped',
75 self::OPTIONAL
76 );
77 $this->addOption(
78 'priority-languages',
79 'Comma separated list of priority language codes',
80 self::OPTIONAL,
81 self::HAS_ARG
82 );
83 $this->addOption(
84 'priority-languages-reason',
85 'Reason for setting the priority languages',
86 self::OPTIONAL,
87 self::HAS_ARG
88 );
89 $this->addOption(
90 'force-priority-languages',
91 'Only allow translations to the priority languages',
92 self::OPTIONAL
93 );
94 $this->addOption(
95 'disallow-transclusion',
96 'Disable translation aware transclusion for this page',
97 self::OPTIONAL
98 );
99 $this->addOption(
100 'use-old-syntax-version',
101 'Use the old syntax version for translatable pages',
102 self::OPTIONAL
103 );
104
105 $this->requireExtension( 'Translate' );
106 }
107
109 public function execute() {
110 $this->pageImportCount = 0;
111 $importFilePath = $this->getPathOfFileToImport();
112 $importUser = $this->getImportUser();
113 $comment = $this->getOption( 'comment' );
114 $interwikiPrefix = $this->getInterwikiPrefix();
115 $assignKnownUsers = $this->hasOption( 'assign-known-users' );
116 $targetPage = $this->getTargetPageName();
117 $translatablePageSettings = $this->getTranslatablePageSettings();
118
119 // First import the page
120 try {
121 $this->totalPagesBeingImported = substr_count( file_get_contents( $importFilePath ), '</page>' );
122 $importer = Services::getInstance()->getTranslatableBundleImporter();
123 $this->output( "Starting import for file '$importFilePath'...\n" );
124 $importer->setPageImportCompleteCallback( [ $this, 'logPageImportComplete' ] );
125 $bundleTitle = $importer->import(
126 $importFilePath,
127 $interwikiPrefix,
128 $assignKnownUsers,
129 $importUser,
130 RequestContext::getMain(),
131 $targetPage,
132 $comment
133 );
134 } catch ( TranslatableBundleImportException $e ) {
135 $this->error( "An error occurred during import: {$e->getMessage()}\n" );
136 $this->error( "Stacktrace: {$e->getTraceAsString()} .\n" );
137 $this->fatalError( 'Stopping import.' );
138 }
139
140 $this->output(
141 "Translatable bundle {$bundleTitle->getPrefixedText()} was imported. " .
142 "Total {$this->pageImportCount} page(s) were created\n"
143 );
144
145 $this->output( "Now marking {$bundleTitle->getPrefixedText()} for translation...\n" );
146 // Try to mark the page for translation
147 $this->markPageForTranslation( $bundleTitle, $translatablePageSettings, $importUser );
148 }
149
150 public function logPageImportComplete( Title $title, ForeignTitle $foreignTitle ): void {
151 ++$this->pageImportCount;
152 $currentProgress = str_pad(
153 (string)$this->pageImportCount,
154 strlen( (string)$this->totalPagesBeingImported ),
155 ' ',
156 STR_PAD_LEFT
157 );
158
159 $progressCounter = "($currentProgress/$this->totalPagesBeingImported)";
160 $this->output( "$progressCounter {$foreignTitle->getFullText()} --> {$title->getFullText()}\n" );
161 }
162
163 private function getPathOfFileToImport(): string {
164 $xmlPath = $this->getArg( 'xml-path' );
165 if ( !file_exists( $xmlPath ) ) {
166 $this->fatalError( "File '$xmlPath' does not exist" );
167 }
168
169 if ( !is_readable( $xmlPath ) ) {
170 $this->fatalError( "File '$xmlPath' is not readable" );
171 }
172
173 return $xmlPath;
174 }
175
176 private function getImportUser(): UserIdentity {
177 $username = $this->getOption( 'user' );
178
179 $userFactory = MediaWikiServices::getInstance()->getUserFactory();
180 $user = $userFactory->newFromName( $username );
181
182 if ( $user === null || !$user->isNamed() ) {
183 $this->fatalError( "User $username does not exist." );
184 }
185
186 return $user;
187 }
188
189 private function getInterwikiPrefix(): string {
190 $interwikiPrefix = trim( $this->getOption( 'interwiki-prefix', '' ) );
191 if ( $interwikiPrefix === '' ) {
192 $this->fatalError( 'Argument interwiki-prefix cannot be empty.' );
193 }
194
195 return $interwikiPrefix;
196 }
197
198 private function getPriorityLanguages(): array {
199 $priorityLanguageCodes = self::commaList2Array( $this->getOption( 'priority-languages' ) ?? '' );
200 $knownLanguageCodes = array_keys( Utilities::getLanguageNames( 'en' ) );
201 $invalidLanguageCodes = array_diff( $priorityLanguageCodes, $knownLanguageCodes );
202
203 if ( $invalidLanguageCodes ) {
204 $this->fatalError(
205 'Unknown priority language code(s): ' . implode( ', ', $invalidLanguageCodes )
206 );
207 }
208
209 return $priorityLanguageCodes;
210 }
211
212 private function markPageForTranslation(
213 Title $bundleTitle,
214 TranslatablePageSettings $translatablePageSettings,
215 UserIdentity $importUser
216 ): void {
217 $translatablePageMarker = Services::getInstance()->getTranslatablePageMarker();
218 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $importUser );
219 try {
220 $operation = $translatablePageMarker->getMarkOperation(
221 $bundleTitle->toPageRecord( IDBAccessObject::READ_LATEST ),
222 null,
223 $translatablePageSettings->shouldTranslateTitle()
224 );
225 } catch ( TranslatablePageMarkException $e ) {
226 $this->error( "Error while marking page {$bundleTitle->getPrefixedText()} for translation.\n" );
227 $this->error( "Fix the issues and mark the page for translation using Special:PageTranslation.\n\n" );
228 $this->fatalError( wfMessage( $e->getMessageObject() )->text() );
229 }
230
231 $statusFormatter = MediaWikiServices::getInstance()
232 ->getFormatterFactory()
233 ->getStatusFormatter( RequestContext::getMain() );
234
235 $unitNameValidationResult = $operation->getUnitValidationStatus();
236 if ( !$unitNameValidationResult->isOK() ) {
237 $this->output( "Unit validation failed for {$bundleTitle->getPrefixedText()}.\n" );
238 $this->fatalError( $statusFormatter->getMessage( $unitNameValidationResult )->text() );
239 }
240
241 try {
242 $translatablePageMarker->markForTranslation(
243 $operation,
244 $translatablePageSettings,
245 RequestContext::getMain(),
246 $user
247 );
248 $this->output( "The page {$bundleTitle->getPrefixedText()} has been marked for translation.\n" );
249 } catch ( TranslatablePageMarkException $e ) {
250 $this->error( "Error while marking page {$bundleTitle->getPrefixedText()} for translation.\n" );
251 $this->error( "Fix the issues and mark the page for translation using Special:PageTranslation.\n\n" );
252 $this->fatalError( $e->getMessageObject()->text() );
253 }
254 }
255
256 private function getTranslatablePageSettings(): TranslatablePageSettings {
257 return new TranslatablePageSettings(
258 $this->getPriorityLanguages(),
259 $this->hasOption( 'force-priority-languages' ),
260 $this->getOption( 'priority-languages-reason' ) ?? '',
261 [],
262 !$this->hasOption( 'skip-translating-title' ),
263 !$this->hasOption( 'use-old-syntax-version' ),
264 !$this->hasOption( 'disallow-transclusion' ),
265 );
266 }
267
268 private function getTargetPageName(): ?Title {
269 $targetPage = $this->getOption( 'target-name' );
270 if ( $targetPage === null ) {
271 return null;
272 }
273
274 try {
275 $targetPageTitle = MediaWikiServices::getInstance()->getTitleFactory()->newFromTextThrow( $targetPage );
276 } catch ( MalformedTitleException $e ) {
277 $this->fatalError(
278 "Target page name $targetPage does not appear to be valid: {$e->getMessage()}"
279 );
280 }
281
282 $shouldOverride = $this->hasOption( 'override' );
283 if ( $targetPageTitle->exists() && !$shouldOverride ) {
284 $this->fatalError(
285 "Specified target page $targetPage already exists. Use '--override' if you still want to import"
286 );
287 }
288
289 if ( !$targetPageTitle->canExist() ) {
290 $this->fatalError( "The target page name $targetPage cannot be created" );
291 }
292
293 return $targetPageTitle;
294 }
295}
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
Exception thrown when an error occurs when importing a translatable bundle via TranslatableBundleImpo...
Exception thrown when TranslatablePageMarker is unable to unmark a page for translation.
Value object containing user configurable settings when marking a page for translation.
Minimal service container.
Definition Services.php:59
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