Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.83% covered (warning)
59.83%
140 / 234
44.44% covered (danger)
44.44%
16 / 36
CRAP
0.00% covered (danger)
0.00%
0 / 1
TranslatablePage
60.09% covered (warning)
60.09%
140 / 233
44.44% covered (danger)
44.44%
16 / 36
362.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 newFromText
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 newFromRevision
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 newFromTitle
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageIdentity
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getText
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
7.66
 getRevision
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSourceLanguageCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMessageGroupId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMessageGroupIdFromTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMessageGroup
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
4.12
 hasPageDisplayTitle
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getPageDisplayTitle
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getStrippedSourcePageText
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getTranslationPageFromTitle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getTranslationPage
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 addMarkedTag
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addReadyTag
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getMarkedTag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReadyTag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTranslationUrl
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getMarkedRevs
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getTranslationPages
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 getTranslationUnitPages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTranslationPercentages
15.79% covered (danger)
15.79%
3 / 19
0.00% covered (danger)
0.00%
0 / 1
19.93
 getTransRev
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 supportsTransclusion
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getRevisionRecordWithFallback
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 isMoveable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isDeletable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isTranslationPage
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
 parseTranslationUnit
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 isSourcePage
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 clearSourcePageCache
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 determineStatus
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\PageTranslation;
5
6use IDBAccessObject;
7use LogicException;
8use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
9use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore;
10use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundle;
11use MediaWiki\Extension\Translate\MessageLoading\MessageHandle;
12use MediaWiki\Extension\Translate\Services;
13use MediaWiki\Extension\Translate\Utilities\Utilities;
14use MediaWiki\Languages\LanguageNameUtils;
15use MediaWiki\Linker\LinkTarget;
16use MediaWiki\MediaWikiServices;
17use MediaWiki\Page\PageIdentity;
18use MediaWiki\Page\PageReference;
19use MediaWiki\Revision\RevisionRecord;
20use MediaWiki\Revision\SlotRecord;
21use MediaWiki\Title\Title;
22use MessageGroupStats;
23use RuntimeException;
24use SpecialPage;
25use TextContent;
26use Wikimedia\Rdbms\Database;
27use Wikimedia\Rdbms\IResultWrapper;
28use Wikimedia\Rdbms\SelectQueryBuilder;
29use WikiPageMessageGroup;
30
31/**
32 * Mixed bag of methods related to translatable pages.
33 * @author Niklas Laxström
34 * @license GPL-2.0-or-later
35 * @ingroup PageTranslation
36 */
37class TranslatablePage extends TranslatableBundle {
38    /**
39     * List of keys in the metadata table that need to be handled for moves and deletions
40     * @phpcs-require-sorted-array
41     */
42    public const METADATA_KEYS = [
43        'maxid',
44        'priorityforce',
45        'prioritylangs',
46        'priorityreason',
47        'transclusion',
48        'version'
49    ];
50    /** @var string Name of the section which contains the translated page title. */
51    public const DISPLAY_TITLE_UNIT_ID = 'Page display title';
52
53    protected PageIdentity $title;
54    /** @var RevTagStore */
55    protected $revTagStore;
56    /** @var ?string Text contents of the page. */
57    protected $text;
58    /** @var ?int Revision of the page, if applicable. */
59    protected $revision;
60    /** @var string From which source this object was constructed: text, revision or title */
61    protected $source;
62    /** @var ?bool Whether the title should be translated */
63    protected $pageDisplayTitle;
64    /** @var ?string */
65    private $targetLanguage;
66
67    protected function __construct( PageIdentity $title ) {
68        $this->title = $title;
69        $this->revTagStore = Services::getInstance()->getRevTagStore();
70    }
71
72    /**
73     * Constructs a translatable page from given text.
74     * Some functions will fail unless you set revision
75     * parameter manually.
76     */
77    public static function newFromText( Title $title, string $text ): self {
78        $obj = new self( $title );
79        $obj->text = $text;
80        $obj->source = 'text';
81
82        return $obj;
83    }
84
85    /**
86     * Constructs a translatable page from given revision.
87     * The revision must belong to the title given or unspecified
88     * behavior will happen.
89     */
90    public static function newFromRevision( PageIdentity $title, int $revision ): self {
91        $rev = MediaWikiServices::getInstance()
92            ->getRevisionLookup()
93            ->getRevisionByTitle( $title, $revision );
94        if ( $rev === null ) {
95            throw new RuntimeException( 'Revision is null' );
96        }
97
98        $obj = new self( $title );
99        $obj->source = 'revision';
100        $obj->revision = $revision;
101
102        return $obj;
103    }
104
105    /**
106     * Constructs a translatable page from title.
107     * The text of last marked revision is loaded when needed.
108     */
109    public static function newFromTitle( PageIdentity $title ): self {
110        $obj = new self( $title );
111        $obj->source = 'title';
112
113        return $obj;
114    }
115
116    /** @inheritDoc */
117    public function getTitle(): Title {
118        return Title::castFromPageIdentity( $this->title );
119    }
120
121    public function getPageIdentity(): PageIdentity {
122        return $this->title;
123    }
124
125    /** Returns the text for this translatable page. */
126    public function getText(): string {
127        if ( $this->text !== null ) {
128            return $this->text;
129        }
130
131        if ( $this->source === 'title' ) {
132            $revision = $this->getMarkedTag();
133            if ( !is_int( $revision ) ) {
134                throw new LogicException(
135                    "Trying to load a text for {$this->getPageIdentity()} which is not marked for translation"
136                );
137            }
138            $this->revision = $revision;
139        }
140
141        $flags = Utilities::shouldReadFromPrimary()
142            ? IDBAccessObject::READ_LATEST
143            : IDBAccessObject::READ_NORMAL;
144        $rev = MediaWikiServices::getInstance()
145            ->getRevisionLookup()
146            ->getRevisionByTitle( $this->getPageIdentity(), $this->revision, $flags );
147        $content = $rev->getContent( SlotRecord::MAIN );
148        $text = ( $content instanceof TextContent ) ? $content->getText() : null;
149
150        if ( !is_string( $text ) ) {
151            throw new RuntimeException( "Failed to load text for {$this->getPageIdentity()}" );
152        }
153
154        $this->text = $text;
155
156        return $this->text;
157    }
158
159    /**
160     * Revision is null if object was constructed using newFromText.
161     * @return null|int
162     */
163    public function getRevision(): ?int {
164        return $this->revision;
165    }
166
167    /**
168     * Returns the source language of this translatable page. In other words
169     * the language in which the page without language code is written.
170     * @since 2013-01-28
171     */
172    public function getSourceLanguageCode(): string {
173        return $this->getTitle()->getPageLanguage()->getCode();
174    }
175
176    /** @inheritDoc */
177    public function getMessageGroupId(): string {
178        return self::getMessageGroupIdFromTitle( $this->getPageIdentity() );
179    }
180
181    /** Constructs MessageGroup id for any title. */
182    public static function getMessageGroupIdFromTitle( PageReference $page ): string {
183        return 'page-' . MediaWikiServices::getInstance()->getTitleFormatter()->getPrefixedText( $page );
184    }
185
186    /**
187     * Returns MessageGroup used for translating this page. It may still be empty
188     * if the page has not been ever marked.
189     */
190    public function getMessageGroup(): ?WikiPageMessageGroup {
191        $groupId = $this->getMessageGroupId();
192        $group = MessageGroups::getGroup( $groupId );
193        if ( !$group || $group instanceof WikiPageMessageGroup ) {
194            return $group;
195        }
196
197        throw new RuntimeException(
198            "Expected $groupId to be of type WikiPageMessageGroup; got " .
199            get_class( $group )
200        );
201    }
202
203    /** Check whether title is marked for translation */
204    public function hasPageDisplayTitle(): bool {
205        // Cached value
206        if ( $this->pageDisplayTitle !== null ) {
207            return $this->pageDisplayTitle;
208        }
209
210        // Check if title section exists in list of sections
211        $factory = Services::getInstance()->getTranslationUnitStoreFactory();
212        $store = $factory->getReader( $this->getPageIdentity() );
213        $this->pageDisplayTitle = in_array( self::DISPLAY_TITLE_UNIT_ID, $store->getNames() );
214
215        return $this->pageDisplayTitle;
216    }
217
218    /** Get translated page title. */
219    public function getPageDisplayTitle( string $languageCode ): ?string {
220        // Return null if title not marked for translation
221        if ( !$this->hasPageDisplayTitle() ) {
222            return null;
223        }
224
225        // Display title from DB
226        $section = str_replace( ' ', '_', self::DISPLAY_TITLE_UNIT_ID );
227        $page = MediaWikiServices::getInstance()->getTitleFormatter()->getPrefixedDBkey( $this->getPageIdentity() );
228
229        try {
230            $group = $this->getMessageGroup();
231        } catch ( RuntimeException $e ) {
232            return null;
233        }
234
235        // Sanity check, seems to happen during moves
236        if ( !$group ) {
237            return null;
238        }
239
240        return $group->getMessage( "$page/$section", $languageCode, IDBAccessObject::READ_NORMAL );
241    }
242
243    public function getStrippedSourcePageText(): string {
244        $parser = Services::getInstance()->getTranslatablePageParser();
245        $text = $parser->cleanupTags( $this->getText() );
246        $text = preg_replace( '~<languages\s*/>\n?~s', '', $text );
247
248        return $text;
249    }
250
251    public static function getTranslationPageFromTitle( Title $title ): ?TranslationPage {
252        $self = self::isTranslationPage( $title );
253        return $self ? $self->getTranslationPage( $self->targetLanguage ) : null;
254    }
255
256    public function getTranslationPage( string $targetLanguage ): TranslationPage {
257        $mwServices = MediaWikiServices::getInstance();
258        $config = $mwServices->getMainConfig();
259        $services = Services::getInstance();
260        $parser = $services->getTranslatablePageParser();
261        $parserOutput = $parser->parse( $this->getText() );
262        $pageVersion = (int)$services->getMessageGroupMetadata()
263            ->get( $this->getMessageGroupId(), 'version' );
264        $wrapUntranslated = $pageVersion >= 2;
265        $languageFactory = $mwServices->getLanguageFactory();
266
267        return new TranslationPage(
268            $parserOutput,
269            $this->getMessageGroup(),
270            $languageFactory->getLanguage( $targetLanguage ),
271            $languageFactory->getLanguage( $this->getSourceLanguageCode() ),
272            $config->get( 'TranslateKeepOutdatedTranslations' ),
273            $wrapUntranslated,
274            $this->getTitle()
275        );
276    }
277
278    /** Adds a tag which indicates that this page is suitable for translation. */
279    public function addMarkedTag( int $revision, array $value = null ) {
280        $this->revTagStore->replaceTag( $this->getPageIdentity(), RevTagStore::TP_MARK_TAG, $revision, $value );
281        self::clearSourcePageCache();
282    }
283
284    /** Adds a tag which indicates that this page source is ready for marking for translation. */
285    public function addReadyTag( int $revision ): void {
286        $this->revTagStore->replaceTag( $this->getPageIdentity(), RevTagStore::TP_READY_TAG, $revision );
287        if ( !self::isSourcePage( $this->getPageIdentity() ) ) {
288            self::clearSourcePageCache();
289        }
290    }
291
292    /** Returns the latest revision which has marked tag, if any. */
293    public function getMarkedTag(): ?int {
294        return $this->revTagStore->getLatestRevisionWithTag( $this->getPageIdentity(), RevTagStore::TP_MARK_TAG );
295    }
296
297    /** Returns the latest revision which has ready tag, if any. */
298    public function getReadyTag(): ?int {
299        return $this->revTagStore->getLatestRevisionWithTag( $this->getPageIdentity(), RevTagStore::TP_READY_TAG );
300    }
301
302    /**
303     * Produces a link to translation view of a translation page.
304     * @param string|bool $code MediaWiki language code. Default: false.
305     * @return string Relative url
306     */
307    public function getTranslationUrl( $code = false ): string {
308        $params = [
309            'group' => $this->getMessageGroupId(),
310            'action' => 'page',
311            'filter' => '',
312            'language' => $code,
313        ];
314
315        $translate = SpecialPage::getTitleFor( 'Translate' );
316
317        return $translate->getLocalURL( $params );
318    }
319
320    public function getMarkedRevs(): IResultWrapper {
321        $db = Utilities::getSafeReadDB();
322
323        return $db->newSelectQueryBuilder()
324            ->select( [ 'rt_revision', 'rt_value' ] )
325            ->from( 'revtag' )
326            ->where( [
327                'rt_page' => $this->getPageIdentity()->getId(),
328                'rt_type' => RevTagStore::TP_MARK_TAG,
329            ] )
330            ->orderBy( 'rt_revision', SelectQueryBuilder::SORT_DESC )
331            ->caller( __METHOD__ )
332            ->fetchResultSet();
333    }
334
335    /** @inheritDoc */
336    public function getTranslationPages(): array {
337        $mwServices = MediaWikiServices::getInstance();
338
339        $messageGroup = $this->getMessageGroup();
340        $knownLanguageCodes = $messageGroup ? $messageGroup->getTranslatableLanguages() : null;
341        $knownLanguageCodes ??= Utilities::getLanguageNames( LanguageNameUtils::AUTONYMS );
342
343        $prefixedDbTitleKey = $this->getPageIdentity()->getDBkey() . '/';
344        $baseNamespace = $this->getPageIdentity()->getNamespace();
345
346        // Build a link batch query for all translation pages
347        $linkBatch = $mwServices->getLinkBatchFactory()->newLinkBatch();
348        foreach ( array_keys( $knownLanguageCodes ) as $code ) {
349            $linkBatch->add( $baseNamespace, $prefixedDbTitleKey . $code );
350        }
351
352        $translationPages = [];
353        foreach ( $linkBatch->getPageIdentities() as $pageIdentity ) {
354            if ( $pageIdentity->exists() ) {
355                $translationPages[] = Title::castFromPageIdentity( $pageIdentity );
356            }
357        }
358
359        return $translationPages;
360    }
361
362    /** @inheritDoc */
363    public function getTranslationUnitPages( ?string $code = null ): array {
364        return $this->getTranslationUnitPagesByTitle( $this->title, $code );
365    }
366
367    public function getTranslationPercentages(): array {
368        // Calculate percentages for the available translations
369        try {
370            $group = $this->getMessageGroup();
371        } catch ( RuntimeException $e ) {
372            return [];
373        }
374
375        if ( !$group ) {
376            return [];
377        }
378
379        $titles = $this->getTranslationPages();
380        $temp = MessageGroupStats::forGroup( $this->getMessageGroupId() );
381        $stats = [];
382
383        foreach ( $titles as $t ) {
384            $handle = new MessageHandle( $t );
385            $code = $handle->getCode();
386
387            // Sometimes we want to display 0.00 for pages for which translation
388            // hasn't started yet.
389            $stats[$code] = 0.00;
390            if ( ( $temp[$code][MessageGroupStats::TOTAL] ?? 0 ) > 0 ) {
391                $total = $temp[$code][MessageGroupStats::TOTAL];
392                $translated = $temp[$code][MessageGroupStats::TRANSLATED];
393                $percentage = $translated / $total;
394                $stats[$code] = sprintf( '%.2f', $percentage );
395            }
396        }
397
398        // Content language is always up-to-date
399        $stats[$this->getSourceLanguageCode()] = 1.00;
400
401        return $stats;
402    }
403
404    public function getTransRev( string $suffix ) {
405        $title = Title::makeTitle( NS_TRANSLATIONS, $suffix );
406
407        $db = Utilities::getSafeReadDB();
408
409        return $db->newSelectQueryBuilder()
410            ->select( 'rt_value' )
411            ->from( 'revtag' )
412            ->where( [
413                'rt_page' => $title->getArticleID(),
414                'rt_type' => RevTagStore::TRANSVER_PROP,
415            ] )
416            ->orderBy( 'rt_revision', SelectQueryBuilder::SORT_DESC )
417            ->caller( __METHOD__ )
418            ->fetchField();
419    }
420
421    public function supportsTransclusion(): ?bool {
422        $transclusion = Services::getInstance()
423            ->getMessageGroupMetadata()
424            ->get( $this->getMessageGroupId(), 'transclusion' );
425        if ( $transclusion === false ) {
426            return null;
427        }
428
429        return $transclusion === '1';
430    }
431
432    public function getRevisionRecordWithFallback(): ?RevisionRecord {
433        $title = $this->getTitle();
434        $store = MediaWikiServices::getInstance()->getRevisionStore();
435        $revRecord = $store->getRevisionByTitle( $title->getSubpage( $this->targetLanguage ) );
436        if ( $revRecord ) {
437            return $revRecord;
438        }
439
440        // Fetch the source fallback
441        return $store->getRevisionByTitle( $title->getSubpage( $this->getSourceLanguageCode() ) );
442    }
443
444    /** @inheritDoc */
445    public function isMoveable(): bool {
446        return $this->getMarkedTag() !== null;
447    }
448
449    /** @inheritDoc */
450    public function isDeletable(): bool {
451        return $this->getMarkedTag() !== null;
452    }
453
454    /** @return bool|self */
455    public static function isTranslationPage( Title $title ) {
456        $handle = new MessageHandle( $title );
457        if ( !Utilities::isTranslationPage( $handle ) ) {
458            return false;
459        }
460
461        $languageCode = $handle->getCode();
462        $newTitle = $handle->getTitleForBase();
463
464        $page = self::newFromTitle( $newTitle );
465
466        if ( $page->getMarkedTag() === null ) {
467            return false;
468        }
469
470        $page->targetLanguage = $languageCode;
471
472        return $page;
473    }
474
475    /** Helper to guess translation page from translation unit. */
476    public static function parseTranslationUnit( LinkTarget $translationUnit ): array {
477        // Format is Translations:SourcePageNamespace:SourcePageName/SectionName/LanguageCode.
478        // We will drop the namespace immediately here.
479        $parts = explode( '/', $translationUnit->getText() );
480
481        // LanguageCode and SectionName are guaranteed to not have '/'.
482        $language = array_pop( $parts );
483        $section = array_pop( $parts );
484        $sourcepage = implode( '/', $parts );
485
486        return [
487            'sourcepage' => $sourcepage,
488            'section' => $section,
489            'language' => $language
490        ];
491    }
492
493    public static function isSourcePage( PageIdentity $page ): bool {
494        if ( !$page->exists() ) {
495            // No point in loading all translatable pages if the page
496            // doesn’t exist. This also avoids PreconditionExceptions
497            // if $page is a Title pointing to a non-proper page like
498            // a special page.
499            return false;
500        }
501
502        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
503        $cacheKey = $cache->makeKey( 'pagetranslation', 'sourcepages' );
504
505        $translatablePageIds = $cache->getWithSetCallback(
506            $cacheKey,
507            $cache::TTL_HOUR * 2,
508            static function ( $oldValue, &$ttl, array &$setOpts ) {
509                $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA );
510                $setOpts += Database::getCacheSetOptions( $dbr );
511
512                return RevTagStore::getTranslatableBundleIds(
513                    RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG
514                );
515            },
516            [
517                'checkKeys' => [ $cacheKey ],
518                'pcTTL' => $cache::TTL_PROC_SHORT,
519                'pcGroup' => __CLASS__ . ':1',
520                'version' => 2,
521            ]
522        );
523
524        return isset( $translatablePageIds[$page->getId()] );
525    }
526
527    /** Clears the source page cache */
528    public static function clearSourcePageCache(): void {
529        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
530        $cache->touchCheckKey( $cache->makeKey( 'pagetranslation', 'sourcepages' ) );
531    }
532
533    public static function determineStatus(
534        ?int $readyRevisionId,
535        ?int $markRevisionId,
536        int $latestRevisionId
537    ): ?TranslatablePageStatus {
538        $status = null;
539        if ( $markRevisionId === null ) {
540            // Never marked, check that the latest version is ready
541            if ( $readyRevisionId === $latestRevisionId ) {
542                $status = TranslatablePageStatus::PROPOSED;
543            } else {
544                // Otherwise, ignore such pages
545                return null;
546            }
547        } elseif ( $readyRevisionId === $latestRevisionId ) {
548            if ( $markRevisionId === $readyRevisionId ) {
549                // Marked and latest version is fine
550                $status = TranslatablePageStatus::ACTIVE;
551            } else {
552                $status = TranslatablePageStatus::OUTDATED;
553            }
554        } else {
555            // Marked but latest version is not fine
556            $status = TranslatablePageStatus::BROKEN;
557        }
558
559        return new TranslatablePageStatus( $status );
560    }
561}
562
563class_alias( TranslatablePage::class, 'TranslatablePage' );