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