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