2declare( strict_types = 1 );
4namespace MediaWiki\Extension\Translate\PageTranslation;
12use MediaWiki\Languages\LanguageNameUtils;
13use MediaWiki\Linker\LinkTarget;
14use MediaWiki\MediaWikiServices;
15use MediaWiki\Page\PageIdentity;
16use MediaWiki\Page\PageReference;
17use MediaWiki\Revision\RevisionLookup;
18use MediaWiki\Revision\RevisionRecord;
19use MediaWiki\Revision\SlotRecord;
27use Wikimedia\Rdbms\Database;
28use Wikimedia\Rdbms\IResultWrapper;
51 public const DISPLAY_TITLE_UNIT_ID =
'Page display title';
53 protected PageIdentity $title;
55 protected $revTagStore;
63 protected $pageDisplayTitle;
65 private $targetLanguage;
67 protected function __construct( PageIdentity $title ) {
68 $this->title = $title;
69 $this->revTagStore = Services::getInstance()->getRevTagStore();
77 public static function newFromText( Title $title,
string $text ): self {
78 $obj = new self( $title );
80 $obj->source =
'text';
90 public static function newFromRevision( PageIdentity $title,
int $revision ): self {
91 $rev = MediaWikiServices::getInstance()
93 ->getRevisionByTitle( $title, $revision );
94 if ( $rev ===
null ) {
95 throw new RuntimeException(
'Revision is null' );
98 $obj =
new self( $title );
99 $obj->source =
'revision';
100 $obj->revision = $revision;
110 $obj = new self( $title );
111 $obj->source =
'title';
118 return Title::newFromPageIdentity( $this->title );
121 public function getPageIdentity(): PageIdentity {
127 if ( $this->text !== null ) {
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"
138 $this->revision = $revision;
141 $flags = Utilities::shouldReadFromPrimary()
142 ? RevisionLookup::READ_LATEST
143 : RevisionLookup::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;
150 if ( !is_string( $text ) ) {
151 throw new RuntimeException(
"Failed to load text for {$this->getPageIdentity()}" );
173 return $this->getTitle()->getPageLanguage()->getCode();
178 return self::getMessageGroupIdFromTitle( $this->getPageIdentity() );
183 return 'page-' . MediaWikiServices::getInstance()->getTitleFormatter()->getPrefixedText( $page );
191 $groupId = $this->getMessageGroupId();
192 $group = MessageGroups::getGroup( $groupId );
197 throw new RuntimeException(
198 "Expected $groupId to be of type WikiPageMessageGroup; got " .
206 if ( $this->pageDisplayTitle !== null ) {
207 return $this->pageDisplayTitle;
211 $factory = Services::getInstance()->getTranslationUnitStoreFactory();
212 $store = $factory->getReader( $this->getPageIdentity() );
213 $this->pageDisplayTitle = in_array( self::DISPLAY_TITLE_UNIT_ID, $store->getNames() );
215 return $this->pageDisplayTitle;
221 if ( !$this->hasPageDisplayTitle() ) {
226 $section = str_replace(
' ',
'_', self::DISPLAY_TITLE_UNIT_ID );
227 $page = MediaWikiServices::getInstance()->getTitleFormatter()->getPrefixedDBkey( $this->getPageIdentity() );
230 $group = $this->getMessageGroup();
231 }
catch ( RuntimeException $e ) {
240 return $group->getMessage(
"$page/$section", $languageCode, $group::READ_NORMAL );
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 );
251 public static function getTranslationPageFromTitle( Title $title ): ?TranslationPage {
252 $self = self::isTranslationPage( $title );
253 return $self ? $self->getTranslationPage( $self->targetLanguage ) :
null;
256 public function getTranslationPage(
string $targetLanguage ): TranslationPage {
257 $mwServices = MediaWikiServices::getInstance();
258 $config = $mwServices->getMainConfig();
259 $parser = Services::getInstance()->getTranslatablePageParser();
260 $parserOutput = $parser->parse( $this->getText() );
261 $pageVersion = (int)TranslateMetadata::get( $this->getMessageGroupId(),
'version' );
262 $wrapUntranslated = $pageVersion >= 2;
263 $languageFactory = $mwServices->getLanguageFactory();
265 return new TranslationPage(
267 $this->getMessageGroup(),
268 $languageFactory->getLanguage( $targetLanguage ),
269 $languageFactory->getLanguage( $this->getSourceLanguageCode() ),
270 $config->get(
'TranslateKeepOutdatedTranslations' ),
278 $this->revTagStore->replaceTag( $this->getPageIdentity(), RevTagStore::TP_MARK_TAG, $revision, $value );
279 self::clearSourcePageCache();
284 $this->revTagStore->replaceTag( $this->getPageIdentity(),
RevTagStore::TP_READY_TAG, $revision );
285 if ( !self::isSourcePage( $this->getPageIdentity() ) ) {
286 self::clearSourcePageCache();
292 return $this->revTagStore->getLatestRevisionWithTag( $this->getPageIdentity(),
RevTagStore::TP_MARK_TAG );
297 return $this->revTagStore->getLatestRevisionWithTag( $this->getPageIdentity(),
RevTagStore::TP_READY_TAG );
305 $tpPageStore =
Services::getInstance()->getTranslatablePageStore();
306 $tpPageStore->unmark( $this->getPageIdentity() );
316 'group' => $this->getMessageGroupId(),
322 $translate = SpecialPage::getTitleFor(
'Translate' );
324 return $translate->getLocalURL( $params );
327 public function getMarkedRevs(): IResultWrapper {
330 $fields = [
'rt_revision',
'rt_value' ];
332 'rt_page' => $this->getPageIdentity()->getId(),
333 'rt_type' => RevTagStore::TP_MARK_TAG,
335 $options = [
'ORDER BY' =>
'rt_revision DESC' ];
337 return $db->select(
'revtag', $fields, $conds, __METHOD__, $options );
342 $mwServices = MediaWikiServices::getInstance();
344 $messageGroup = $this->getMessageGroup();
345 $knownLanguageCodes = $messageGroup ? $messageGroup->getTranslatableLanguages() :
null;
346 $knownLanguageCodes ??= Utilities::getLanguageNames( LanguageNameUtils::AUTONYMS );
348 $prefixedDbTitleKey = $this->getPageIdentity()->getDBkey() .
'/';
349 $baseNamespace = $this->getPageIdentity()->getNamespace();
352 $linkBatch = $mwServices->getLinkBatchFactory()->newLinkBatch();
353 foreach ( array_keys( $knownLanguageCodes ) as $code ) {
354 $linkBatch->add( $baseNamespace, $prefixedDbTitleKey . $code );
357 $translationPages = [];
358 foreach ( $linkBatch->getPageIdentities() as $pageIdentity ) {
359 if ( $pageIdentity->exists() ) {
360 $translationPages[] = Title::castFromPageIdentity( $pageIdentity );
364 return $translationPages;
369 return $this->getTranslationUnitPagesByTitle( $this->title, $code );
372 public function getTranslationPercentages(): array {
375 $group = $this->getMessageGroup();
376 }
catch ( RuntimeException $e ) {
384 $titles = $this->getTranslationPages();
385 $temp = MessageGroupStats::forGroup( $this->getMessageGroupId() );
388 foreach ( $titles as $t ) {
390 $code = $handle->getCode();
394 $stats[$code] = 0.00;
395 if ( ( $temp[$code][MessageGroupStats::TOTAL] ?? 0 ) > 0 ) {
396 $total = $temp[$code][MessageGroupStats::TOTAL];
397 $translated = $temp[$code][MessageGroupStats::TRANSLATED];
398 $percentage = $translated / $total;
399 $stats[$code] = sprintf(
'%.2f', $percentage );
404 $stats[$this->getSourceLanguageCode()] = 1.00;
409 public function getTransRev(
string $suffix ) {
410 $title = Title::makeTitle( NS_TRANSLATIONS, $suffix );
412 $db = Utilities::getSafeReadDB();
413 $fields =
'rt_value';
415 'rt_page' => $title->getArticleID(),
416 'rt_type' => RevTagStore::TRANSVER_PROP,
418 $options = [
'ORDER BY' =>
'rt_revision DESC' ];
420 return $db->selectField(
'revtag', $fields, $conds, __METHOD__, $options );
423 public function supportsTransclusion(): ?bool {
424 $transclusion =
TranslateMetadata::get( $this->getMessageGroupId(),
'transclusion' );
425 if ( $transclusion ===
false ) {
429 return $transclusion ===
'1';
432 public function setTransclusion(
bool $supportsTransclusion ): void {
434 $this->getMessageGroupId(),
436 $supportsTransclusion ?
'1' :
'0'
440 public function getRevisionRecordWithFallback(): ?RevisionRecord {
441 $title = $this->getTitle();
442 $store = MediaWikiServices::getInstance()->getRevisionStore();
443 $revRecord = $store->getRevisionByTitle( $title->getSubpage( $this->targetLanguage ) );
449 return $store->getRevisionByTitle( $title->getSubpage( $this->getSourceLanguageCode() ) );
454 return $this->getMarkedTag() !== null;
459 return $this->getMarkedTag() !== null;
465 if ( !Utilities::isTranslationPage( $handle ) ) {
469 $languageCode = $handle->getCode();
470 $newTitle = $handle->getTitleForBase();
476 $page = self::newFromTitle( $newTitle );
478 if ( $page->getMarkedTag() ===
null ) {
482 $page->targetLanguage = $languageCode;
491 $parts = explode(
'/', $translationUnit->getText() );
494 $language = array_pop( $parts );
495 $section = array_pop( $parts );
496 $sourcepage = implode(
'/', $parts );
499 'sourcepage' => $sourcepage,
500 'section' => $section,
501 'language' => $language
505 public static function isSourcePage( PageIdentity $page ): bool {
506 if ( !$page->exists() ) {
514 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
515 $cacheKey = $cache->makeKey(
'pagetranslation',
'sourcepages' );
517 $translatablePageIds = $cache->getWithSetCallback(
519 $cache::TTL_HOUR * 2,
520 static function ( $oldValue, &$ttl, array &$setOpts ) {
521 $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA );
522 $setOpts += Database::getCacheSetOptions( $dbr );
524 return RevTagStore::getTranslatableBundleIds(
525 RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG
529 'checkKeys' => [ $cacheKey ],
530 'pcTTL' => $cache::TTL_PROC_SHORT,
531 'pcGroup' => __CLASS__ .
':1',
536 return isset( $translatablePageIds[$page->getId()] );
541 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
542 $cache->touchCheckKey( $cache->makeKey(
'pagetranslation',
'sourcepages' ) );
545 public static function determineStatus(
546 ?
int $readyRevisionId,
547 ?
int $markRevisionId,
548 int $latestRevisionId
551 if ( $markRevisionId ===
null ) {
553 if ( $readyRevisionId === $latestRevisionId ) {
554 $status = TranslatablePageStatus::PROPOSED;
559 } elseif ( $readyRevisionId === $latestRevisionId ) {
560 if ( $markRevisionId === $readyRevisionId ) {
562 $status = TranslatablePageStatus::ACTIVE;
564 $status = TranslatablePageStatus::OUTDATED;
568 $status = TranslatablePageStatus::BROKEN;
571 return new TranslatablePageStatus( $status );
575class_alias( TranslatablePage::class,
'TranslatablePage' );
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
Stores and validates possible statuses for TranslatablePage.
Mixed bag of methods related to translatable pages.
static newFromRevision(PageIdentity $title, int $revision)
Constructs a translatable page from given revision.
addReadyTag(int $revision)
Adds a tag which indicates that this page source is ready for marking for translation.
static clearSourcePageCache()
Clears the source page cache.
getMessageGroupId()
@inheritDoc
getTranslationUnitPages(?string $code=null)
@inheritDoc
static newFromTitle(PageIdentity $title)
Constructs a translatable page from title.
getMarkedTag()
Returns the latest revision which has marked tag, if any.
getText()
Returns the text for this translatable page.
hasPageDisplayTitle()
Check whether title is marked for translation.
static getMessageGroupIdFromTitle(PageReference $page)
Constructs MessageGroup id for any title.
getTranslationPages()
@inheritDoc
getReadyTag()
Returns the latest revision which has ready tag, if any.
addMarkedTag(int $revision, array $value=null)
Adds a tag which indicates that this page is suitable for translation.
getRevision()
Revision is null if object was constructed using newFromText.
getSourceLanguageCode()
Returns the source language of this translatable page.
unmarkTranslatablePage()
Removes all page translation feature data from the database.
getTranslationUrl( $code=false)
Produces a link to translation view of a translation page.
getMessageGroup()
Returns MessageGroup used for translating this page.
const METADATA_KEYS
List of keys in the metadata table that need to be handled for moves and deletions @phpcs-require-sor...
static parseTranslationUnit(LinkTarget $translationUnit)
Helper to guess translation page from translation unit.
static newFromText(Title $title, string $text)
Constructs a translatable page from given text.
getPageDisplayTitle(string $languageCode)
Get translated page title.
static isTranslationPage(Title $title)
This class abstract MessageGroup statistics calculation and storing.
Class for pointing to messages, like Title class is for titles.
Wraps the translatable page sections into a message group.