Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ExportTranslationsSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
5
7use Html;
8use HTMLForm;
9use LogicException;
15use Message;
16use MessageGroup;
18use ParserFactory;
19use SpecialPage;
20use Status;
21use Title;
22use TitleFormatter;
24
33class ExportTranslationsSpecialPage extends SpecialPage {
35 public const MAX_EXPORT_SIZE = 10000;
37 protected $language;
39 protected $format;
41 protected $groupId;
43 private $titleFormatter;
45 private $parserFactory;
47 private const VALID_FORMATS = [ 'export-as-po', 'export-to-file', 'export-as-csv' ];
48
49 public function __construct( TitleFormatter $titleFormatter, ParserFactory $parserFactory ) {
50 parent::__construct( 'ExportTranslations' );
51 $this->titleFormatter = $titleFormatter;
52 $this->parserFactory = $parserFactory;
53 }
54
56 public function execute( $par ) {
57 $out = $this->getOutput();
58 $request = $this->getRequest();
59 $lang = $this->getLanguage();
60
61 $this->setHeaders();
62
63 $this->groupId = $request->getText( 'group', $par ?? '' );
64 $this->language = $request->getVal( 'language', $lang->getCode() );
65 $this->format = $request->getText( 'format' );
66
67 $this->outputForm();
68 $out->addModules( 'ext.translate.special.exporttranslations' );
69
70 if ( $this->groupId ) {
71 $status = $this->checkInput();
72 if ( !$status->isGood() ) {
73 $out->wrapWikiTextAsInterface(
74 'error',
75 $status->getWikiText( false, false, $lang )
76 );
77 return;
78 }
79
80 $status = $this->doExport();
81 if ( !$status->isGood() ) {
82 $out->addHTML(
83 Html::errorBox( $status->getHTML( false, false, $lang ) )
84 );
85 }
86 }
87 }
88
89 private function outputForm(): void {
90 $fields = [
91 'group' => [
92 'type' => 'select',
93 'name' => 'group',
94 'id' => 'group',
95 'label-message' => 'translate-page-group',
96 'options' => $this->getGroupOptions(),
97 'default' => $this->groupId,
98 ],
99 'language' => [
100 // @todo Apply ULS to this field
101 'type' => 'select',
102 'name' => 'language',
103 'id' => 'language',
104 'label-message' => 'translate-page-language',
105 'options' => $this->getLanguageOptions(),
106 'default' => $this->language,
107 ],
108 'format' => [
109 'type' => 'radio',
110 'name' => 'format',
111 'id' => 'format',
112 'label-message' => 'translate-export-form-format',
113 'flatlist' => true,
114 'options' => $this->getFormatOptions(),
115 'default' => $this->format,
116 ],
117 ];
118 HTMLForm::factory( 'ooui', $fields, $this->getContext() )
119 ->setMethod( 'get' )
120 ->setId( 'mw-export-message-group-form' )
121 ->setWrapperLegendMsg( 'translate-page-settings-legend' )
122 ->setSubmitTextMsg( 'translate-submit' )
123 ->prepareForm()
124 ->displayForm( false );
125 }
126
127 private function getGroupOptions(): array {
128 $selected = $this->groupId;
129 $groups = MessageGroups::getAllGroups();
130 uasort( $groups, [ MessageGroups::class, 'groupLabelSort' ] );
131
132 $options = [];
133 foreach ( $groups as $id => $group ) {
134 if ( !$group->exists()
135 || ( MessageGroups::getPriority( $group ) === 'discouraged' && $id !== $selected )
136 ) {
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( 'ignored' );
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.
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
Class for pointing to messages, like Title class is for titles.
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.