Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ExportTranslationsSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
5
7use LogicException;
14use MediaWiki\Html\Html;
15use MediaWiki\HTMLForm\HTMLForm;
16use MediaWiki\Language\FormatterFactory;
17use MediaWiki\Message\Message;
18use MediaWiki\Parser\ParserFactory;
19use MediaWiki\SpecialPage\SpecialPage;
20use MediaWiki\Status\Status;
21use MediaWiki\Status\StatusFormatter;
22use MediaWiki\Title\Title;
23use MediaWiki\Title\TitleFormatter;
24use MessageGroup;
26
35class ExportTranslationsSpecialPage extends SpecialPage {
37 public const MAX_EXPORT_SIZE = 10000;
38 protected string $language;
39 protected string $format;
40 protected string $groupId;
41 private TitleFormatter $titleFormatter;
42 private ParserFactory $parserFactory;
43 private StatusFormatter $statusFormatter;
45 private const VALID_FORMATS = [ 'export-as-po', 'export-to-file', 'export-as-csv' ];
46
47 public function __construct(
48 TitleFormatter $titleFormatter,
49 ParserFactory $parserFactory,
50 FormatterFactory $formatterFactory
51 ) {
52 parent::__construct( 'ExportTranslations' );
53 $this->titleFormatter = $titleFormatter;
54 $this->parserFactory = $parserFactory;
55 $this->statusFormatter = $formatterFactory->getStatusFormatter( $this );
56 }
57
59 public function execute( $par ) {
60 $out = $this->getOutput();
61 $request = $this->getRequest();
62 $lang = $this->getLanguage();
63
64 $this->setHeaders();
65
66 $this->groupId = $request->getText( 'group', $par ?? '' );
67 $this->language = $request->getVal( 'language', $lang->getCode() );
68 $this->format = $request->getText( 'format' );
69
70 $this->outputForm();
71 $out->addModules( 'ext.translate.special.exporttranslations' );
72
73 if ( $this->groupId ) {
74 $status = $this->checkInput();
75 if ( !$status->isGood() ) {
76 $out->wrapWikiTextAsInterface(
77 'error',
78 $this->statusFormatter->getWikiText( $status )
79 );
80 return;
81 }
82
83 $status = $this->doExport();
84 if ( !$status->isGood() ) {
85 $out->addHTML(
86 Html::errorBox( $this->statusFormatter->getHTML( $status, [ 'lang' => $lang ] ) )
87 );
88 }
89 }
90 }
91
92 private function outputForm(): void {
93 $fields = [
94 'group' => [
95 'type' => 'select',
96 'name' => 'group',
97 'id' => 'group',
98 'label-message' => 'translate-page-group',
99 'options' => $this->getGroupOptions(),
100 'default' => $this->groupId,
101 ],
102 'language' => [
103 // @todo Apply ULS to this field
104 'type' => 'select',
105 'name' => 'language',
106 'id' => 'language',
107 'label-message' => 'translate-page-language',
108 'options' => $this->getLanguageOptions(),
109 'default' => $this->language,
110 ],
111 'format' => [
112 'type' => 'radio',
113 'name' => 'format',
114 'id' => 'format',
115 'label-message' => 'translate-export-form-format',
116 'flatlist' => true,
117 'options' => $this->getFormatOptions(),
118 'default' => $this->format,
119 ],
120 ];
121 HTMLForm::factory( 'ooui', $fields, $this->getContext() )
122 ->setMethod( 'get' )
123 ->setId( 'mw-export-message-group-form' )
124 ->setWrapperLegendMsg( 'translate-page-settings-legend' )
125 ->setSubmitTextMsg( 'translate-submit' )
126 ->prepareForm()
127 ->displayForm( false );
128 }
129
130 private function getGroupOptions(): array {
131 $groups = MessageGroups::getAllGroups();
132 uasort( $groups, [ MessageGroups::class, 'groupLabelSort' ] );
133
134 $options = [];
135 foreach ( $groups as $id => $group ) {
136 if ( !$group->exists() ) {
137 continue;
138 }
139
140 $options[$group->getLabel()] = $id;
141 }
142
143 return $options;
144 }
145
147 private function getLanguageOptions(): array {
148 $languages = Utilities::getLanguageNames( 'en' );
149 $options = [];
150 foreach ( $languages as $code => $name ) {
151 $options["$code - $name"] = $code;
152 }
153
154 return $options;
155 }
156
158 private function getFormatOptions(): array {
159 $options = [];
160 foreach ( self::VALID_FORMATS as $format ) {
161 // translate-taskui-export-to-file, translate-taskui-export-as-po
162 $options[ $this->msg( "translate-taskui-$format" )->escaped() ] = $format;
163 }
164 return $options;
165 }
166
167 private function checkInput(): Status {
168 $status = Status::newGood();
169
170 $msgGroup = MessageGroups::getGroup( $this->groupId );
171 if ( $msgGroup === null ) {
172 $status->fatal( 'translate-page-no-such-group' );
173 } elseif ( MessageGroups::isDynamic( $msgGroup ) ) {
174 $status->fatal( 'translate-export-not-supported' );
175 }
176
177 $langNames = Utilities::getLanguageNames( 'en' );
178 if ( !isset( $langNames[$this->language] ) ) {
179 $status->fatal( 'translate-page-no-such-language' );
180 }
181
182 // Do not show this error if invalid format is specified for translatable page
183 // groups as we can show a textarea box containing the translation page text
184 // (however it's not currently supported for other groups).
185 if (
186 !$msgGroup instanceof WikiPageMessageGroup
187 && $this->format
188 && !in_array( $this->format, self::VALID_FORMATS )
189 ) {
190 $status->fatal( 'translate-export-invalid-format' );
191 }
192
193 if ( $this->format === 'export-to-file'
194 && !$msgGroup instanceof FileBasedMessageGroup
195 ) {
196 $status->fatal( 'translate-export-format-notsupported' );
197 }
198
199 if ( $msgGroup && !MessageGroups::isDynamic( $msgGroup ) ) {
200 $size = count( $msgGroup->getKeys() );
201 if ( $size > self::MAX_EXPORT_SIZE ) {
202 $status->fatal(
203 'translate-export-group-too-large',
204 Message::numParam( self::MAX_EXPORT_SIZE )
205 );
206 }
207 }
208
209 return $status;
210 }
211
212 private function doExport(): Status {
213 $out = $this->getOutput();
214 $group = MessageGroups::getGroup( $this->groupId );
215 $collection = $this->setupCollection( $group );
216
217 switch ( $this->format ) {
218 case 'export-as-po':
219 $out->disable();
220
221 $fileFormat = null;
222 if ( $group instanceof FileBasedMessageGroup ) {
223 $fileFormat = $group->getFFS();
224 }
225
226 if ( !$fileFormat instanceof GettextFormat ) {
227 if ( !$group instanceof FileBasedMessageGroup ) {
229 }
230
231 $fileFormat = new GettextFormat( $group );
232 }
233
234 $fileFormat->setOfflineMode( true );
235
236 $filename = "{$group->getId()}_{$this->language}.po";
237 $this->sendExportHeaders( $filename );
238
239 echo $fileFormat->writeIntoVariable( $collection );
240 break;
241
242 case 'export-to-file':
243 // This will never happen since its checked previously but add the check to keep
244 // phan and IDE happy. See checkInput method
245 if ( !$group instanceof FileBasedMessageGroup ) {
246 throw new LogicException(
247 "'export-to-file' requested for a non FileBasedMessageGroup {$group->getId()}"
248 );
249 }
250
251 $messages = $group->getFFS()->writeIntoVariable( $collection );
252
253 if ( $messages === '' ) {
254 return Status::newFatal( 'translate-export-format-file-empty' );
255 }
256
257 $out->disable();
258 $filename = basename( $group->getSourceFilePath( $collection->getLanguage() ) );
259 $this->sendExportHeaders( $filename );
260 echo $messages;
261 break;
262
263 case 'export-as-csv':
264 $out->disable();
265 $filename = "{$group->getId()}_{$this->language}.csv";
266 $this->sendExportHeaders( $filename );
267 $this->exportCSV( $collection, $group->getSourceLanguage() );
268 break;
269
270 default:
271 // @todo Add web viewing for groups other than WikiPageMessageGroup
272 if ( !$group instanceof WikiPageMessageGroup ) {
273 return Status::newFatal( 'translate-export-format-notsupported' );
274 }
275
276 $translatablePage = TranslatablePage::newFromTitle( $group->getTitle() );
277 $translationPage = $translatablePage->getTranslationPage( $collection->getLanguage() );
278
279 $translationPage->filterMessageCollection( $collection );
280 $text = $translationPage->generateSourceFromMessageCollection(
281 $this->parserFactory->getInstance(),
282 $collection
283 );
284
285 $displayTitle = $translatablePage->getPageDisplayTitle( $this->language );
286 if ( $displayTitle ) {
287 $text = "{{DISPLAYTITLE:$displayTitle}}$text";
288 }
289
290 $box = Html::element(
291 'textarea',
292 [ 'id' => 'wpTextbox', 'rows' => 40, ],
293 $text
294 );
295 $out->addHTML( $box );
296
297 }
298
299 return Status::newGood();
300 }
301
302 private function setupCollection( MessageGroup $group ): MessageCollection {
303 $collection = $group->initCollection( $this->language );
304
305 // Don't export ignored, unless it is the source language or message documentation
306 $translateDocCode = $this->getConfig()->get( 'TranslateDocumentationLanguageCode' );
307 if ( $this->language !== $translateDocCode
308 && $this->language !== $group->getSourceLanguage()
309 ) {
310 $collection->filter( MessageCollection::FILTER_IGNORED, MessageCollection::EXCLUDE_MATCHING );
311 }
312
313 $collection->loadTranslations();
314
315 return $collection;
316 }
317
319 private function sendExportHeaders( string $fileName ): void {
320 $response = $this->getRequest()->response();
321 $response->header( 'Content-Type: text/plain; charset=UTF-8' );
322 $response->header( "Content-Disposition: attachment; filename=\"$fileName\"" );
323 }
324
325 private function exportCSV( MessageCollection $collection, string $sourceLanguageCode ): void {
326 $fp = fopen( 'php://output', 'w' );
327 $exportingSourceLanguage = $sourceLanguageCode === $this->language;
328
329 $header = [
330 $this->msg( 'translate-export-csv-message-title' )->text(),
331 $this->msg( 'translate-export-csv-definition' )->text()
332 ];
333
334 if ( !$exportingSourceLanguage ) {
335 $header[] = $this->language;
336 }
337
338 fputcsv( $fp, $header );
339
340 foreach ( $collection->keys() as $messageKey => $titleValue ) {
341 $message = $collection[ $messageKey ];
342 $prefixedTitleText = $this->titleFormatter->getPrefixedText( $titleValue );
343
344 $handle = new MessageHandle( Title::newFromText( $prefixedTitleText ) );
345 $sourceLanguageTitle = $handle->getTitleForLanguage( $sourceLanguageCode );
346
347 $row = [ $sourceLanguageTitle->getPrefixedText(), $message->definition() ];
348
349 if ( !$exportingSourceLanguage ) {
350 $row[] = $message->translation();
351 }
352
353 fputcsv( $fp, $row );
354 }
355
356 fclose( $fp );
357 }
358
359 protected function getGroupName() {
360 return 'translation';
361 }
362}
This class implements default behavior for file based message groups.
static newFromMessageGroup(MessageGroup $group, string $targetPattern='')
Constructs a FileBasedMessageGroup from any normal message group.
FileFormat class that implements support for gettext file format.
Factory class for accessing message groups individually by id or all of them as a list.
This file contains the class for core message collections implementation.
Class for pointing to messages, like Title class is for titles.
Mixed bag of methods related to translatable pages.
This special page allows exporting groups for offline translation.
const MAX_EXPORT_SIZE
Maximum size of a group until exporting is not allowed due to performance reasons.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31
Wraps the translatable page sections into a message group.
Interface for message groups.
getSourceLanguage()
Returns language code depicting the language of source text.
Finds external changes for file based message groups.