Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
GettextFormat.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\FileFormatSupport;
5
6use InvalidArgumentException;
7use LanguageCode;
13use MediaWiki\Logger\LoggerFactory;
14use MediaWiki\MediaWikiServices;
15use MediaWiki\Title\Title;
16use RuntimeException;
17use SpecialVersion;
18
29 private bool $allowPotMode = false;
30 private bool $offlineMode = false;
31
32 public function supportsFuzzy(): string {
33 return 'yes';
34 }
35
36 public function getFileExtensions(): array {
37 return [ '.pot', '.po' ];
38 }
39
40 public function setOfflineMode( bool $value ): void {
41 $this->offlineMode = $value;
42 }
43
45 public function read( $languageCode ) {
46 // This is somewhat hacky, but pot mode should only ever be used for the source language.
47 // See https://phabricator.wikimedia.org/T230361
48 $this->allowPotMode = $this->getGroup()->getSourceLanguage() === $languageCode;
49
50 try {
51 return parent::read( $languageCode );
52 } finally {
53 $this->allowPotMode = false;
54 }
55 }
56
57 public function readFromVariable( string $data ): array {
58 # Authors first
59 $matches = [];
60 preg_match_all( '/^#\s*Author:\s*(.*)$/m', $data, $matches );
61 $authors = $matches[1];
62
63 # Then messages and everything else
64 $parsedData = $this->parseGettext( $data );
65 $parsedData['AUTHORS'] = $authors;
66
67 foreach ( $parsedData['MESSAGES'] as $key => $value ) {
68 if ( $value === '' ) {
69 unset( $parsedData['MESSAGES'][$key] );
70 }
71 }
72
73 return $parsedData;
74 }
75
76 private function parseGettext( string $data ): array {
77 $mangler = $this->group->getMangler();
78 $useCtxtAsKey = $this->extra['CtxtAsKey'] ?? false;
79 $keyAlgorithm = 'simple';
80 if ( isset( $this->extra['keyAlgorithm'] ) ) {
81 $keyAlgorithm = $this->extra['keyAlgorithm'];
82 }
83
84 $potmode = false;
85
86 // Normalise newlines, to make processing easier
87 $data = str_replace( "\r\n", "\n", $data );
88
89 /* Delimit the file into sections, which are separated by two newlines.
90 * We are permissive and accept more than two. This parsing method isn't
91 * efficient wrt memory, but was easy to implement */
92 $sections = preg_split( '/\n{2,}/', $data );
93
94 /* First one isn't an actual message. We'll handle it specially below */
95 $headerSection = array_shift( $sections );
96 /* Since this is the header section, we are only interested in the tags
97 * and msgid is empty. Somewhere we should extract the header comments
98 * too */
99 $match = $this->expectKeyword( 'msgstr', $headerSection );
100 if ( $match !== null ) {
101 $headerBlock = $this->formatForWiki( $match, 'trim' );
102 $headers = $this->parseHeaderTags( $headerBlock );
103
104 // Check for pot-mode by checking if the header is fuzzy
105 $flags = $this->parseFlags( $headerSection );
106 if ( in_array( 'fuzzy', $flags, true ) ) {
107 $potmode = $this->allowPotMode;
108 }
109 } else {
110 $message = "Gettext file header was not found:\n\n$data";
111 throw new GettextParseException( $message );
112 }
113
114 $template = [];
115 $messages = [];
116
117 // Extract some metadata from headers for easier use
118 $metadata = [];
119 if ( isset( $headers['X-Language-Code'] ) ) {
120 $metadata['code'] = $headers['X-Language-Code'];
121 }
122
123 if ( isset( $headers['X-Message-Group'] ) ) {
124 $metadata['group'] = $headers['X-Message-Group'];
125 }
126
127 /* At this stage we are only interested how many plurals forms we should
128 * be expecting when parsing the rest of this file. */
129 $pluralCount = null;
130 if ( $potmode ) {
131 $pluralCount = 2;
132 } elseif ( isset( $headers['Plural-Forms'] ) ) {
133 $pluralCount = $metadata['plural'] = GettextPlural::getPluralCount( $headers['Plural-Forms'] );
134 }
135
136 $metadata['plural'] = $pluralCount;
137
138 // Then parse the messages
139 foreach ( $sections as $section ) {
140 $item = $this->parseGettextSection( $section, $pluralCount );
141 if ( $item === null ) {
142 continue;
143 }
144
145 if ( $useCtxtAsKey ) {
146 if ( !isset( $item['ctxt'] ) ) {
147 error_log( "ctxt missing for: $section" );
148 continue;
149 }
150 $key = $item['ctxt'];
151 } else {
152 $key = $this->generateKeyFromItem( $item, $keyAlgorithm );
153 }
154
155 $key = $mangler->mangle( $key );
156 $messages[$key] = $potmode ? $item['id'] : $item['str'];
157 $template[$key] = $item;
158 }
159
160 return [
161 'MESSAGES' => $messages,
162 'EXTRA' => [
163 'TEMPLATE' => $template,
164 'METADATA' => $metadata,
165 'HEADERS' => $headers,
166 ],
167 ];
168 }
169
170 private function parseGettextSection( string $section, ?int $pluralCount ): ?array {
171 if ( trim( $section ) === '' ) {
172 return null;
173 }
174
175 /* These inactive sections are of no interest to us. Multiline mode
176 * is needed because there may be flags or other annoying stuff
177 * before the commented out sections.
178 */
179 if ( preg_match( '/^#~/m', $section ) ) {
180 return null;
181 }
182
183 $item = [
184 'ctxt' => false,
185 'id' => '',
186 'str' => '',
187 'flags' => [],
188 'comments' => [],
189 ];
190
191 $match = $this->expectKeyword( 'msgid', $section );
192 if ( $match !== null ) {
193 $item['id'] = $this->formatForWiki( $match );
194 } else {
195 throw new RuntimeException( "Unable to parse msgid:\n\n$section" );
196 }
197
198 $match = $this->expectKeyword( 'msgctxt', $section );
199 if ( $match !== null ) {
200 $item['ctxt'] = $this->formatForWiki( $match );
201 }
202
203 $pluralMessage = false;
204 $match = $this->expectKeyword( 'msgid_plural', $section );
205 if ( $match !== null ) {
206 $pluralMessage = true;
207 $plural = $this->formatForWiki( $match );
208 $item['id'] = GettextPlural::flatten( [ $item['id'], $plural ] );
209 }
210
211 if ( $pluralMessage ) {
212 $pluralMessageText = $this->processGettextPluralMessage( $pluralCount, $section );
213
214 // Keep the translation empty if no form has translation
215 if ( $pluralMessageText !== '' ) {
216 $item['str'] = $pluralMessageText;
217 }
218 } else {
219 $match = $this->expectKeyword( 'msgstr', $section );
220 if ( $match !== null ) {
221 $item['str'] = $this->formatForWiki( $match );
222 } else {
223 throw new RuntimeException( "Unable to parse msgstr:\n\n$section" );
224 }
225 }
226
227 // Parse flags
228 $flags = $this->parseFlags( $section );
229 foreach ( $flags as $key => $flag ) {
230 if ( $flag === 'fuzzy' ) {
231 $item['str'] = TRANSLATE_FUZZY . $item['str'];
232 unset( $flags[$key] );
233 }
234 }
235 $item['flags'] = $flags;
236
237 // Rest of the comments
238 $matches = [];
239 if ( preg_match_all( '/^#(.?) (.*)$/m', $section, $matches, PREG_SET_ORDER ) ) {
240 foreach ( $matches as $match ) {
241 if ( $match[1] !== ',' && !str_starts_with( $match[1], '[Wiki]' ) ) {
242 $item['comments'][$match[1]][] = $match[2];
243 }
244 }
245 }
246
247 return $item;
248 }
249
250 private function processGettextPluralMessage( ?int $pluralCount, string $section ): string {
251 $actualForms = [];
252
253 for ( $i = 0; $i < $pluralCount; $i++ ) {
254 $match = $this->expectKeyword( "msgstr\\[$i\\]", $section );
255
256 if ( $match !== null ) {
257 $actualForms[] = $this->formatForWiki( $match );
258 } else {
259 $actualForms[] = '';
260 error_log( "Plural $i not found, expecting total of $pluralCount for $section" );
261 }
262 }
263
264 if ( array_sum( array_map( 'strlen', $actualForms ) ) > 0 ) {
265 return GettextPlural::flatten( $actualForms );
266 } else {
267 return '';
268 }
269 }
270
271 private function parseFlags( string $section ): array {
272 $matches = [];
273 if ( preg_match( '/^#,(.*)$/mu', $section, $matches ) ) {
274 return array_map( 'trim', explode( ',', $matches[1] ) );
275 } else {
276 return [];
277 }
278 }
279
280 private function expectKeyword( string $name, string $section ): ?string {
281 /* Catches the multiline textblock that comes after keywords msgid,
282 * msgstr, msgid_plural, msgctxt.
283 */
284 $poformat = '".*"\n?(^".*"$\n?)*';
285
286 $matches = [];
287 if ( preg_match( "/^$name\s($poformat)/mx", $section, $matches ) ) {
288 return $matches[1];
289 } else {
290 return null;
291 }
292 }
293
300 public function generateKeyFromItem( array $item, string $algorithm = 'simple' ): string {
301 $lang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( 'en' );
302
303 if ( $item['ctxt'] === '' ) {
304 /* Messages with msgctxt as empty string should be different
305 * from messages without any msgctxt. To avoid BC break make
306 * the empty ctxt a special case */
307 $hash = sha1( $item['id'] . 'MSGEMPTYCTXT' );
308 } else {
309 $hash = sha1( $item['ctxt'] . $item['id'] );
310 }
311
312 if ( $algorithm === 'simple' ) {
313 $hash = substr( $hash, 0, 6 );
314 $snippet = $lang->truncateForDatabase( $item['id'], 30, '' );
315 $snippet = str_replace( ' ', '_', trim( $snippet ) );
316 } else { // legacy
317 $legalChars = Title::legalChars();
318 $snippet = $item['id'];
319 $snippet = preg_replace( "/[^$legalChars]/", ' ', $snippet );
320 $snippet = preg_replace( "/[:&%\/_]/", ' ', $snippet );
321 $snippet = preg_replace( '/ {2,}/', ' ', $snippet );
322 $snippet = $lang->truncateForDatabase( $snippet, 30, '' );
323 $snippet = str_replace( ' ', '_', trim( $snippet ) );
324 }
325
326 return "$hash-$snippet";
327 }
328
332 private function processData( string $data ): string {
333 $quotePattern = '/(^"|"$\n?)/m';
334 $data = preg_replace( $quotePattern, '', $data );
335 return stripcslashes( $data );
336 }
337
342 private function handleWhitespace( string $data, string $whitespace ): string {
343 if ( preg_match( '/\s$/', $data ) ) {
344 if ( $whitespace === 'mark' ) {
345 $data .= '\\';
346 } elseif ( $whitespace === 'trim' ) {
347 $data = rtrim( $data );
348 } else {
349 // This condition will never happen as long as $whitespace is 'mark' or 'trim'
350 throw new InvalidArgumentException( "Unknown action for whitespace: $whitespace" );
351 }
352 }
353
354 return $data;
355 }
356
363 private function formatForWiki( string $data, string $whitespace = 'mark' ): string {
364 $data = $this->processData( $data );
365 return $this->handleWhitespace( $data, $whitespace );
366 }
367
368 private function parseHeaderTags( string $headers ): array {
369 $tags = [];
370 foreach ( explode( "\n", $headers ) as $line ) {
371 if ( !str_contains( $line, ':' ) ) {
372 error_log( __METHOD__ . ": $line" );
373 }
374 [ $key, $value ] = explode( ':', $line, 2 );
375 $tags[trim( $key )] = trim( $value );
376 }
377
378 return $tags;
379 }
380
381 protected function writeReal( MessageCollection $collection ): string {
382 // FIXME: this should be the source language
383 $pot = $this->read( 'en' ) ?? [];
384 $code = $collection->code;
385 $template = $this->read( $code ) ?? [];
386 $output = $this->doGettextHeader( $collection, $template['EXTRA'] ?? [] );
387
388 $pluralRule = GettextPlural::getPluralRule( $code );
389 if ( !$pluralRule ) {
390 $pluralRule = GettextPlural::getPluralRule( 'en' );
391 LoggerFactory::getInstance( 'Translate' )->warning(
392 "T235180: Missing Gettext plural rule for '{languagecode}'",
393 [ 'languagecode' => $code ]
394 );
395 }
396 $pluralCount = GettextPlural::getPluralCount( $pluralRule );
397
398 $documentationLanguageCode = MediaWikiServices::getInstance()
399 ->getMainConfig()
400 ->get( 'TranslateDocumentationLanguageCode' );
401 $documentationCollection = null;
402 if ( is_string( $documentationLanguageCode ) ) {
403 $documentationCollection = clone $collection;
404 $documentationCollection->resetForNewLanguage( $documentationLanguageCode );
405 $documentationCollection->loadTranslations();
406 }
407
409 foreach ( $collection as $key => $m ) {
410 $transTemplate = $template['EXTRA']['TEMPLATE'][$key] ?? [];
411 $potTemplate = $pot['EXTRA']['TEMPLATE'][$key] ?? [];
412 $documentation = isset( $documentationCollection[$key] ) ?
413 $documentationCollection[$key]->translation() : null;
414
415 $output .= $this->formatMessageBlock(
416 $key,
417 $m,
418 $transTemplate,
419 $potTemplate,
420 $pluralCount,
421 $documentation
422 );
423 }
424
425 return $output;
426 }
427
428 private function doGettextHeader( MessageCollection $collection, array $template ): string {
429 global $wgSitename;
430
431 $code = $collection->code;
432 $name = Utilities::getLanguageName( $code );
433 $native = Utilities::getLanguageName( $code, $code );
434 $authors = $this->doAuthors( $collection );
435 if ( isset( $this->extra['header'] ) ) {
436 $extra = "# --\n" . $this->extra['header'];
437 } else {
438 $extra = '';
439 }
440
441 $group = $this->getGroup();
442 $output =
443 <<<EOT
444 # Translation of {$group->getLabel()} to $name ($native)
445 # Exported from $wgSitename
446 #
447 $authors$extra
448 EOT;
449
450 // Make sure there is no empty line before msgid
451 $output = trim( $output ) . "\n";
452
453 $specs = $template['HEADERS'] ?? [];
454
455 $timestamp = wfTimestampNow();
456 $specs['PO-Revision-Date'] = $this->formatTime( $timestamp );
457 if ( $this->offlineMode ) {
458 $specs['POT-Creation-Date'] = $this->formatTime( $timestamp );
459 } else {
460 $specs['X-POT-Import-Date'] = $this->formatTime( wfTimestamp( TS_MW, $this->getPotTime() ) );
461 }
462 $specs['Content-Type'] = 'text/plain; charset=UTF-8';
463 $specs['Content-Transfer-Encoding'] = '8bit';
464
465 $specs['Language'] = LanguageCode::bcp47( $this->group->mapCode( $code ) );
466 MediaWikiServices::getInstance()->getHookContainer()
467 ->run( 'Translate:GettextFFS:headerFields', [ &$specs, $this->group, $code ] );
468 $specs['X-Generator'] = 'MediaWiki '
469 . SpecialVersion::getVersion()
470 . '; Translate '
471 . Utilities::getVersion();
472
473 if ( $this->offlineMode ) {
474 $specs['X-Language-Code'] = $code;
475 $specs['X-Message-Group'] = $group->getId();
476 }
477
478 $specs['Plural-Forms'] = GettextPlural::getPluralRule( $code )
479 ?: GettextPlural::getPluralRule( 'en' );
480
481 $output .= 'msgid ""' . "\n";
482 $output .= 'msgstr ""' . "\n";
483 $output .= '""' . "\n";
484
485 foreach ( $specs as $k => $v ) {
486 $output .= $this->escape( "$k: $v\n" ) . "\n";
487 }
488
489 $output .= "\n";
490
491 return $output;
492 }
493
494 private function doAuthors( MessageCollection $collection ): string {
495 $output = '';
496 $authors = $collection->getAuthors();
497 $authors = $this->filterAuthors( $authors, $collection->code );
498
499 foreach ( $authors as $author ) {
500 $output .= "# Author: $author\n";
501 }
502
503 return $output;
504 }
505
506 private function formatMessageBlock(
507 string $key,
508 Message $message,
509 array $trans,
510 array $pot,
511 int $pluralCount,
512 ?string $documentation
513 ): string {
514 $header = $this->formatDocumentation( $documentation );
515 $content = '';
516
517 $comments = $pot['comments'] ?? $trans['comments'] ?? [];
518 foreach ( $comments as $type => $typecomments ) {
519 foreach ( $typecomments as $comment ) {
520 $header .= "#$type $comment\n";
521 }
522 }
523
524 $flags = $pot['flags'] ?? $trans['flags'] ?? [];
525 $flags = array_merge( $message->getTags(), $flags );
526
527 if ( $this->offlineMode ) {
528 $content .= 'msgctxt ' . $this->escape( $key ) . "\n";
529 } else {
530 $ctxt = $pot['ctxt'] ?? $trans['ctxt'] ?? false;
531 if ( $ctxt !== false ) {
532 $content .= 'msgctxt ' . $this->escape( $ctxt ) . "\n";
533 }
534 }
535
536 $msgid = $message->definition();
537 $msgstr = $message->translation() ?? '';
538 if ( strpos( $msgstr, TRANSLATE_FUZZY ) !== false ) {
539 $msgstr = str_replace( TRANSLATE_FUZZY, '', $msgstr );
540 // Might be fuzzy infile
541 $flags[] = 'fuzzy';
542 }
543
544 if ( GettextPlural::hasPlural( $msgid ) ) {
545 $forms = GettextPlural::unflatten( $msgid, 2 );
546 $content .= 'msgid ' . $this->escape( $forms[0] ) . "\n";
547 $content .= 'msgid_plural ' . $this->escape( $forms[1] ) . "\n";
548
549 try {
550 $forms = GettextPlural::unflatten( $msgstr, $pluralCount );
551 foreach ( $forms as $index => $form ) {
552 $content .= "msgstr[$index] " . $this->escape( $form ) . "\n";
553 }
554 } catch ( GettextPluralException $e ) {
555 $flags[] = 'invalid-plural';
556 for ( $i = 0; $i < $pluralCount; $i++ ) {
557 $content .= "msgstr[$i] \"\"\n";
558 }
559 }
560 } else {
561 $content .= 'msgid ' . $this->escape( $msgid ) . "\n";
562 $content .= 'msgstr ' . $this->escape( $msgstr ) . "\n";
563 }
564
565 if ( $flags ) {
566 sort( $flags );
567 $header .= '#, ' . implode( ', ', array_unique( $flags ) ) . "\n";
568 }
569
570 $output = $header ?: "#\n";
571 $output .= $content . "\n";
572
573 return $output;
574 }
575
576 private function formatTime( string $time ): string {
577 $lang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( 'en' );
578
579 return $lang->sprintfDate( 'xnY-xnm-xnd xnH:xni:xns+0000', $time );
580 }
581
582 private function getPotTime(): string {
583 $cache = $this->group->getMessageGroupCache( $this->group->getSourceLanguage() );
584
585 return $cache->exists() ? $cache->getTimestamp() : wfTimestampNow();
586 }
587
588 private function formatDocumentation( ?string $documentation ): string {
589 if ( !is_string( $documentation ) ) {
590 return '';
591 }
592
593 if ( !$this->offlineMode ) {
594 return '';
595 }
596
597 $lines = explode( "\n", $documentation );
598 $out = '';
599 foreach ( $lines as $line ) {
600 $out .= "#. [Wiki] $line\n";
601 }
602
603 return $out;
604 }
605
606 private function escape( string $line ): string {
607 // There may be \ as a last character, for keeping trailing whitespace
608 $line = preg_replace( '/(\s)\\\\$/', '\1', $line );
609 $line = addcslashes( $line, '\\"' );
610 $line = str_replace( "\n", '\n', $line );
611 return '"' . $line . '"';
612 }
613
614 public function shouldOverwrite( string $a, string $b ): bool {
615 $regex = '/^"(.+)-Date: \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\+\d\d\d\d\\\\n"$/m';
616
617 $a = preg_replace( $regex, '', $a );
618 $b = preg_replace( $regex, '', $b );
619
620 return $a !== $b;
621 }
622
623 public static function getExtraSchema(): array {
624 return [
625 'root' => [
626 '_type' => 'array',
627 '_children' => [
628 'FILES' => [
629 '_type' => 'array',
630 '_children' => [
631 'header' => [
632 '_type' => 'text',
633 ],
634 'keyAlgorithm' => [
635 '_type' => 'enum',
636 '_values' => [ 'simple', 'legacy' ],
637 ],
638 'CtxtAsKey' => [
639 '_type' => 'boolean',
640 ],
641 ]
642 ]
643 ]
644 ]
645 ];
646 }
647
648 public function isContentEqual( ?string $a, ?string $b ): bool {
649 if ( $a === $b ) {
650 return true;
651 }
652
653 if ( $a === null || $b === null ) {
654 return false;
655 }
656
657 try {
658 $parsedA = GettextPlural::parsePluralForms( $a );
659 $parsedB = GettextPlural::parsePluralForms( $b );
660
661 // if they have the different number of plural forms, just fail
662 if ( count( $parsedA[1] ) !== count( $parsedB[1] ) ) {
663 return false;
664 }
665
666 } catch ( GettextPluralException $e ) {
667 // Something failed, invalid syntax?
668 return false;
669 }
670
671 $expectedPluralCount = count( $parsedA[1] );
672
673 // GettextPlural::unflatten() will return an empty array when $expectedPluralCount is 0
674 // So if they do not have translations and are different strings, they are not equal
675 if ( $expectedPluralCount === 0 ) {
676 return false;
677 }
678
679 return GettextPlural::unflatten( $a, $expectedPluralCount )
680 === GettextPlural::unflatten( $b, $expectedPluralCount );
681 }
682}
683
684class_alias( GettextFormat::class, 'GettextFFS' );
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
FileFormat class that implements support for gettext file format.
generateKeyFromItem(array $item, string $algorithm='simple')
Generates unique key for each message.
readFromVariable(string $data)
Parse the message data given as a string in the SimpleFormat format and return it as an array of AUTH...
getFileExtensions()
Return the commonly used file extensions for these formats.
A very basic FileFormatSupport module that implements some basic functionality and a simple binary ba...
This file contains the class for core message collections implementation.
Interface for message objects used by MessageCollection.
Definition Message.php:13
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31
Message groups are usually configured in YAML, though the actual storage format does not matter,...