2declare( strict_types = 1 );
4namespace MediaWiki\Extension\Translate\PageTranslation;
10use MediaWiki\Linker\LinkTarget;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Revision\RevisionLookup;
13use MediaWiki\Revision\RevisionRecord;
14use MediaWiki\Revision\SlotRecord;
25use Wikimedia\Rdbms\Database;
26use Wikimedia\Rdbms\IResultWrapper;
49 public const DISPLAY_TITLE_UNIT_ID =
'Page display title';
54 protected $revTagStore;
62 protected $pageDisplayTitle;
64 private $targetLanguage;
68 $this->title = $title;
77 public static function newFromText( Title $title,
string $text ): self {
78 $obj = new self( $title );
80 $obj->source =
'text';
92 $rev = MediaWikiServices::getInstance()
94 ->getRevisionByTitle( $title, $revision );
95 if ( $rev ===
null ) {
96 throw new MWException(
'Revision is null' );
99 $obj =
new self( $title );
100 $obj->source =
'revision';
101 $obj->revision = $revision;
111 $obj = new self( $title );
112 $obj->source =
'title';
124 if ( $this->text !== null ) {
128 $page = $this->getTitle()->getPrefixedDBkey();
130 if ( $this->source ===
'title' ) {
131 $revision = $this->getMarkedTag();
132 if ( !is_int( $revision ) ) {
133 throw new LogicException(
134 "Trying to load a text for $page which is not marked for translation"
137 $this->revision = $revision;
140 $flags = TranslateUtils::shouldReadFromPrimary()
141 ? RevisionLookup::READ_LATEST
142 : RevisionLookup::READ_NORMAL;
143 $rev = MediaWikiServices::getInstance()
144 ->getRevisionLookup()
145 ->getRevisionByTitle( $this->getTitle(), $this->revision, $flags );
146 $content = $rev->getContent( SlotRecord::MAIN );
147 $text = ( $content instanceof TextContent ) ? $content->getText() :
null;
149 if ( !is_string( $text ) ) {
150 throw new RuntimeException(
"Failed to load text for $page" );
172 return $this->getTitle()->getPageLanguage()->getCode();
177 return self::getMessageGroupIdFromTitle( $this->getTitle() );
182 return 'page-' . $title->getPrefixedText();
190 $groupId = $this->getMessageGroupId();
191 $group = MessageGroups::getGroup( $groupId );
196 throw new RuntimeException(
197 "Expected $groupId to be of type WikiPageMessageGroup; got " .
205 if ( $this->pageDisplayTitle !== null ) {
206 return $this->pageDisplayTitle;
210 $factory = Services::getInstance()->getTranslationUnitStoreFactory();
211 $store = $factory->getReader( $this->getTitle() );
212 $this->pageDisplayTitle = in_array( self::DISPLAY_TITLE_UNIT_ID, $store->getNames() );
214 return $this->pageDisplayTitle;
220 if ( !$this->hasPageDisplayTitle() ) {
225 $section = str_replace(
' ',
'_', self::DISPLAY_TITLE_UNIT_ID );
226 $page = $this->getTitle()->getPrefixedDBkey();
229 $group = $this->getMessageGroup();
230 }
catch ( RuntimeException $e ) {
239 return $group->getMessage(
"$page/$section", $languageCode, $group::READ_NORMAL );
242 public function getStrippedSourcePageText(): string {
243 $parser = Services::getInstance()->getTranslatablePageParser();
244 $text = $parser->cleanupTags( $this->getText() );
245 $text = preg_replace(
'~<languages\s*/>\n?~s',
'', $text );
250 public static function getTranslationPageFromTitle( Title $title ): ?TranslationPage {
251 $self = self::isTranslationPage( $title );
256 return $self->getTranslationPage( $self->targetLanguage );
259 public function getTranslationPage(
string $targetLanguage ): TranslationPage {
260 $mwServices = MediaWikiServices::getInstance();
261 $config = $mwServices->getMainConfig();
262 $parser = Services::getInstance()->getTranslatablePageParser();
263 $parserOutput = $parser->parse( $this->getText() );
264 $pageVersion = (int)TranslateMetadata::get( $this->getMessageGroupId(),
'version' );
265 $wrapUntranslated = $pageVersion >= 2;
266 $languageFactory = $mwServices->getLanguageFactory();
268 return new TranslationPage(
270 $this->getMessageGroup(),
271 $languageFactory->getLanguage( $targetLanguage ),
272 $languageFactory->getLanguage( $this->getSourceLanguageCode() ),
273 $config->get(
'TranslateKeepOutdatedTranslations' ),
279 protected static $tagCache = [];
283 $this->revTagStore->replaceTag( $this->getTitle(), RevTagStore::TP_MARK_TAG, $revision, $value );
284 self::clearSourcePageCache();
289 $this->revTagStore->replaceTag( $this->getTitle(),
RevTagStore::TP_READY_TAG, $revision );
290 if ( !self::isSourcePage( $this->getTitle() ) ) {
291 self::clearSourcePageCache();
297 return $this->revTagStore->getLatestRevisionWithTag( $this->getTitle(),
RevTagStore::TP_MARK_TAG );
302 return $this->revTagStore->getLatestRevisionWithTag( $this->getTitle(),
RevTagStore::TP_READY_TAG );
310 $aid = $this->getTitle()->getArticleID();
311 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_PRIMARY );
312 $this->revTagStore->removeTags( $this->getTitle(), RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG );
313 $dbw->delete(
'translate_sections', [
'trs_page' => $aid ], __METHOD__ );
315 self::clearSourcePageCache();
325 'group' => $this->getMessageGroupId(),
331 $translate = SpecialPage::getTitleFor(
'Translate' );
333 return $translate->getLocalURL( $params );
336 public function getMarkedRevs(): IResultWrapper {
339 $fields = [
'rt_revision',
'rt_value' ];
341 'rt_page' => $this->getTitle()->getArticleID(),
342 'rt_type' => RevTagStore::TP_MARK_TAG,
344 $options = [
'ORDER BY' =>
'rt_revision DESC' ];
346 return $db->select(
'revtag', $fields, $conds, __METHOD__, $options );
351 $mwServices = MediaWikiServices::getInstance();
352 $knownLanguageCodes = $this->getMessageGroup()->getTranslatableLanguages()
353 ?? TranslateUtils::getLanguageNames(
null );
355 $prefixedDbTitleKey = $this->getTitle()->getDBkey() .
'/';
356 $baseNamespace = $this->getTitle()->getNamespace();
359 $linkBatch = $mwServices->getLinkBatchFactory()->newLinkBatch();
360 foreach ( array_keys( $knownLanguageCodes ) as $code ) {
361 $linkBatch->add( $baseNamespace, $prefixedDbTitleKey . $code );
364 $translationPages = [];
365 foreach ( $linkBatch->getPageIdentities() as $pageIdentity ) {
366 if ( $pageIdentity->exists() ) {
367 $translationPages[] = Title::castFromPageIdentity( $pageIdentity );
371 return $translationPages;
376 return $this->getTranslationUnitPagesByTitle( $this->title, $code );
379 public function getTranslationPercentages(): array {
382 $group = $this->getMessageGroup();
383 }
catch ( RuntimeException $e ) {
391 $titles = $this->getTranslationPages();
392 $temp = MessageGroupStats::forGroup( $this->getMessageGroupId() );
395 foreach ( $titles as $t ) {
397 $code = $handle->getCode();
401 $stats[$code] = 0.00;
402 if ( ( $temp[$code][MessageGroupStats::TOTAL] ?? 0 ) > 0 ) {
403 $total = $temp[$code][MessageGroupStats::TOTAL];
404 $translated = $temp[$code][MessageGroupStats::TRANSLATED];
405 $percentage = $translated / $total;
406 $stats[$code] = sprintf(
'%.2f', $percentage );
411 $stats[$this->getSourceLanguageCode()] = 1.00;
416 public function getTransRev(
string $suffix ) {
417 $title = Title::makeTitle( NS_TRANSLATIONS, $suffix );
420 $fields =
'rt_value';
422 'rt_page' => $title->getArticleID(),
423 'rt_type' => RevTagStore::TRANSVER_PROP,
425 $options = [
'ORDER BY' =>
'rt_revision DESC' ];
427 return $db->selectField(
'revtag', $fields, $conds, __METHOD__, $options );
430 public function supportsTransclusion(): ?bool {
431 $transclusion =
TranslateMetadata::get( $this->getMessageGroupId(),
'transclusion' );
432 if ( $transclusion ===
false ) {
436 return $transclusion ===
'1';
439 public function setTransclusion(
bool $supportsTransclusion ): void {
441 $this->getMessageGroupId(),
443 $supportsTransclusion ?
'1' :
'0'
447 public function getRevisionRecordWithFallback(): ?RevisionRecord {
448 $title = $this->getTitle();
449 $store = MediaWikiServices::getInstance()->getRevisionStore();
450 $revRecord = $store->getRevisionByTitle( $title->getSubpage( $this->targetLanguage ) );
456 $sourceLanguage = $this->getMessageGroup()->getSourceLanguage();
457 return $store->getRevisionByTitle( $title->getSubpage( $sourceLanguage ) );
462 return $this->getMarkedTag() !== null;
467 return $this->getMarkedTag() !== null;
473 $key = $handle->getKey();
474 $code = $handle->getCode();
476 if ( $key ===
'' || $code ===
'' ) {
480 $codes = MediaWikiServices::getInstance()->getLanguageNameUtils()->getLanguageNames();
481 global $wgTranslateDocumentationLanguageCode;
482 unset( $codes[$wgTranslateDocumentationLanguageCode] );
484 if ( !isset( $codes[$code] ) ) {
488 $newtitle = self::changeTitleText( $title, $key );
494 $page = self::newFromTitle( $newtitle );
496 if ( $page->getMarkedTag() ===
null ) {
500 $page->targetLanguage = $code;
505 private static function changeTitleText( Title $title,
string $text ): ?Title {
506 return Title::makeTitleSafe( $title->getNamespace(), $text );
513 $parts = explode(
'/', $translationUnit->getText() );
516 $language = array_pop( $parts );
517 $section = array_pop( $parts );
518 $sourcepage = implode(
'/', $parts );
521 'sourcepage' => $sourcepage,
522 'section' => $section,
523 'language' => $language
527 public static function isSourcePage( Title $title ): bool {
528 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
529 $cacheKey = $cache->makeKey(
'pagetranslation',
'sourcepages' );
531 $translatablePageIds = $cache->getWithSetCallback(
533 $cache::TTL_HOUR * 2,
534 static function ( $oldValue, &$ttl, array &$setOpts ) {
535 $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA );
536 $setOpts += Database::getCacheSetOptions( $dbr );
538 return RevTagStore::getTranslatableBundleIds(
539 RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG
543 'checkKeys' => [ $cacheKey ],
544 'pcTTL' => $cache::TTL_PROC_SHORT,
545 'pcGroup' => __CLASS__ .
':1'
549 return in_array( $title->getArticleID(), $translatablePageIds );
554 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
555 $cache->touchCheckKey( $cache->makeKey(
'pagetranslation',
'sourcepages' ) );
559class_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'), MessageIndex::singleton());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, '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: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:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, '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: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(), new RevTagStore(), $services->getDBLoadBalancer());}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(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());}, '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
Mixed bag of methods related to translatable pages.
__construct(Title $title)
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
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(Title $title)
Constructs MessageGroup id for any title.
getTranslationPages()
@inheritDoc
static newFromRevision(Title $title, int $revision)
Constructs a translatable page from given revision.
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.
static newFromTitle(Title $title)
Constructs a translatable page from title.
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.
Factory class for accessing message groups individually by id or all of them as an list.
Class for pointing to messages, like Title class is for titles.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
static getSafeReadDB()
Get a DB handle suitable for read and read-for-write cases.
Wraps the translatable page sections into a message group.