Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ExportTranslationsSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
5
7use GettextFFS;
8use Html;
9use HTMLForm;
10use LogicException;
15use Message;
16use MessageGroup;
18use Parser;
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 $parser;
47 private const VALID_FORMATS = [ 'export-as-po', 'export-to-file', 'export-as-csv' ];
48
49 public function __construct( TitleFormatter $titleFormatter, Parser $parser ) {
50 parent::__construct( 'ExportTranslations' );
51 $this->titleFormatter = $titleFormatter;
52 $this->parser = $parser;
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
69 if ( $this->groupId ) {
70 $status = $this->checkInput();
71 if ( !$status->isGood() ) {
72 $out->wrapWikiTextAsInterface(
73 'error',
74 $status->getWikiText( false, false, $lang )
75 );
76 return;
77 }
78
79 $this->doExport();
80 }
81 }
82
83 private function outputForm(): void {
84 $fields = [
85 'group' => [
86 'type' => 'select',
87 'name' => 'group',
88 'id' => 'group',
89 'label-message' => 'translate-page-group',
90 'options' => $this->getGroupOptions(),
91 'default' => $this->groupId,
92 ],
93 'language' => [
94 // @todo Apply ULS to this field
95 'type' => 'select',
96 'name' => 'language',
97 'id' => 'language',
98 'label-message' => 'translate-page-language',
99 'options' => $this->getLanguageOptions(),
100 'default' => $this->language,
101 ],
102 'format' => [
103 'type' => 'radio',
104 'name' => 'format',
105 'id' => 'format',
106 'label-message' => 'translate-export-form-format',
107 'flatlist' => true,
108 'options' => $this->getFormatOptions(),
109 'default' => $this->format,
110 ],
111 ];
112 HTMLForm::factory( 'ooui', $fields, $this->getContext() )
113 ->setMethod( 'get' )
114 ->setWrapperLegendMsg( 'translate-page-settings-legend' )
115 ->setSubmitTextMsg( 'translate-submit' )
116 ->prepareForm()
117 ->displayForm( false );
118 }
119
120 private function getGroupOptions(): array {
121 $selected = $this->groupId;
122 $groups = MessageGroups::getAllGroups();
123 uasort( $groups, [ MessageGroups::class, 'groupLabelSort' ] );
124
125 $options = [];
126 foreach ( $groups as $id => $group ) {
127 if ( !$group->exists()
128 || ( MessageGroups::getPriority( $group ) === 'discouraged' && $id !== $selected )
129 ) {
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(): void {
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 $ffs = null;
215 if ( $group instanceof FileBasedMessageGroup ) {
216 $ffs = $group->getFFS();
217 }
218
219 if ( !$ffs instanceof GettextFFS ) {
220 if ( !$group instanceof FileBasedMessageGroup ) {
222 }
223
224 $ffs = new GettextFFS( $group );
225 }
226
227 $ffs->setOfflineMode( true );
228
229 $filename = "{$group->getId()}_{$this->language}.po";
230 $this->sendExportHeaders( $filename );
231
232 echo $ffs->writeIntoVariable( $collection );
233 break;
234
235 case 'export-to-file':
236 $out->disable();
237
238 // This will never happen since its checked previously but add the check to keep
239 // phan and IDE happy. See checkInput method
240 if ( !$group instanceof FileBasedMessageGroup ) {
241 throw new LogicException(
242 "'export-to-file' requested for a non FileBasedMessageGroup {$group->getId()}"
243 );
244 }
245
246 $filename = basename( $group->getSourceFilePath( $collection->getLanguage() ) );
247 $this->sendExportHeaders( $filename );
248
249 echo $group->getFFS()->writeIntoVariable( $collection );
250 break;
251
252 case 'export-as-csv':
253 $out->disable();
254 $filename = "{$group->getId()}_{$this->language}.csv";
255 $this->sendExportHeaders( $filename );
256 $this->exportCSV( $collection, $group->getSourceLanguage() );
257 break;
258
259 default:
260 // @todo Add web viewing for groups other than WikiPageMessageGroup
261 if ( !$group instanceof WikiPageMessageGroup ) {
262 return;
263 }
264
265 $translatablePage = TranslatablePage::newFromTitle( $group->getTitle() );
266 $translationPage = $translatablePage->getTranslationPage( $collection->getLanguage() );
267
268 $translationPage->filterMessageCollection( $collection );
269 $text = $translationPage->generateSourceFromMessageCollection( $this->parser, $collection );
270
271 $displayTitle = $translatablePage->getPageDisplayTitle( $this->language );
272 if ( $displayTitle ) {
273 $text = "{{DISPLAYTITLE:$displayTitle}}$text";
274 }
275
276 $box = Html::element(
277 'textarea',
278 [ 'id' => 'wpTextbox', 'rows' => 40, ],
279 $text
280 );
281 $out->addHTML( $box );
282
283 }
284 }
285
286 private function setupCollection( MessageGroup $group ): MessageCollection {
287 $collection = $group->initCollection( $this->language );
288
289 // Don't export ignored, unless it is the source language or message documentation
290 $translateDocCode = $this->getConfig()->get( 'TranslateDocumentationLanguageCode' );
291 if ( $this->language !== $translateDocCode
292 && $this->language !== $group->getSourceLanguage()
293 ) {
294 $collection->filter( 'ignored' );
295 }
296
297 $collection->loadTranslations();
298
299 return $collection;
300 }
301
303 private function sendExportHeaders( string $fileName ): void {
304 $response = $this->getRequest()->response();
305 $response->header( 'Content-Type: text/plain; charset=UTF-8' );
306 $response->header( "Content-Disposition: attachment; filename=\"$fileName\"" );
307 }
308
309 private function exportCSV( MessageCollection $collection, string $sourceLanguageCode ): void {
310 $fp = fopen( 'php://output', 'w' );
311 $exportingSourceLanguage = $sourceLanguageCode === $this->language;
312
313 $header = [
314 $this->msg( 'translate-export-csv-message-title' )->text(),
315 $this->msg( 'translate-export-csv-definition' )->text()
316 ];
317
318 if ( !$exportingSourceLanguage ) {
319 $header[] = $this->language;
320 }
321
322 fputcsv( $fp, $header );
323
324 foreach ( $collection->keys() as $messageKey => $titleValue ) {
325 $message = $collection[ $messageKey ];
326 $prefixedTitleText = $this->titleFormatter->getPrefixedText( $titleValue );
327
328 $handle = new MessageHandle( Title::newFromText( $prefixedTitleText ) );
329 $sourceLanguageTitle = $handle->getTitleForLanguage( $sourceLanguageCode );
330
331 $row = [ $sourceLanguageTitle->getPrefixedText(), $message->definition() ];
332
333 if ( !$exportingSourceLanguage ) {
334 $row[] = $message->translation();
335 }
336
337 fputcsv( $fp, $row );
338 }
339
340 fclose( $fp );
341 }
342
343 protected function getGroupName() {
344 return 'translation';
345 }
346}
This class implements default behavior for file based message groups.
static newFromMessageGroup(MessageGroup $group, string $targetPattern='')
Constructs a FileBasedMessageGroup from any normal message group.
New-style FFS 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:30
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.