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