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