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