Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5950655
TranslatablePage.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
TranslatablePage.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
MediaWiki\Extension\Translate\PageTranslation
;
use
IDBAccessObject
;
use
LogicException
;
use
MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups
;
use
MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore
;
use
MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundle
;
use
MediaWiki\Extension\Translate\MessageLoading\MessageHandle
;
use
MediaWiki\Extension\Translate\Services
;
use
MediaWiki\Extension\Translate\Statistics\MessageGroupStats
;
use
MediaWiki\Extension\Translate\Utilities\Utilities
;
use
MediaWiki\Languages\LanguageNameUtils
;
use
MediaWiki\Linker\LinkTarget
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\Page\PageIdentity
;
use
MediaWiki\Page\PageReference
;
use
MediaWiki\Revision\RevisionRecord
;
use
MediaWiki\Revision\SlotRecord
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\Title\Title
;
use
RuntimeException
;
use
TextContent
;
use
Wikimedia\Rdbms\Database
;
use
Wikimedia\Rdbms\IResultWrapper
;
use
Wikimedia\Rdbms\SelectQueryBuilder
;
use
WikiPageMessageGroup
;
/**
* Mixed bag of methods related to translatable pages.
* @author Niklas Laxström
* @license GPL-2.0-or-later
* @ingroup PageTranslation
*/
class
TranslatablePage
extends
TranslatableBundle
{
/**
* List of keys in the metadata table that need to be handled for moves and deletions
* @phpcs-require-sorted-array
*/
public
const
METADATA_KEYS
=
[
'maxid'
,
'priorityforce'
,
'prioritylangs'
,
'priorityreason'
,
'transclusion'
,
'version'
];
/** @var string Name of the section which contains the translated page title. */
public
const
DISPLAY_TITLE_UNIT_ID
=
'Page display title'
;
protected
PageIdentity
$title
;
protected
RevTagStore
$revTagStore
;
/** @var ?string Text contents of the page. */
protected
$text
;
/** @var ?int Revision of the page, if applicable. */
protected
$revision
;
/** @var string From which source this object was constructed: text, revision or title */
protected
$source
;
/** @var ?bool Whether the title should be translated */
protected
$pageDisplayTitle
;
/** @var ?string */
private
$targetLanguage
;
protected
function
__construct
(
PageIdentity
$title
)
{
$this
->
title
=
$title
;
$this
->
revTagStore
=
Services
::
getInstance
()->
getRevTagStore
();
}
/**
* Constructs a translatable page from given text.
* Some functions will fail unless you set revision
* parameter manually.
*/
public
static
function
newFromText
(
Title
$title
,
string
$text
):
self
{
$obj
=
new
self
(
$title
);
$obj
->
text
=
$text
;
$obj
->
source
=
'text'
;
return
$obj
;
}
/**
* Constructs a translatable page from given revision.
* The revision must belong to the title given or unspecified
* behavior will happen.
*/
public
static
function
newFromRevision
(
PageIdentity
$title
,
int
$revision
):
self
{
$rev
=
MediaWikiServices
::
getInstance
()
->
getRevisionLookup
()
->
getRevisionByTitle
(
$title
,
$revision
);
if
(
$rev
===
null
)
{
throw
new
RuntimeException
(
'Revision is null'
);
}
$obj
=
new
self
(
$title
);
$obj
->
source
=
'revision'
;
$obj
->
revision
=
$revision
;
return
$obj
;
}
/**
* Constructs a translatable page from title.
* The text of last marked revision is loaded when needed.
*/
public
static
function
newFromTitle
(
PageIdentity
$title
):
self
{
$obj
=
new
self
(
$title
);
$obj
->
source
=
'title'
;
return
$obj
;
}
/** @inheritDoc */
public
function
getTitle
():
Title
{
return
Title
::
castFromPageIdentity
(
$this
->
title
);
}
public
function
getPageIdentity
():
PageIdentity
{
return
$this
->
title
;
}
/** Returns the text for this translatable page. */
public
function
getText
():
string
{
if
(
$this
->
text
!==
null
)
{
return
$this
->
text
;
}
if
(
$this
->
source
===
'title'
)
{
$revision
=
$this
->
getMarkedTag
();
if
(
!
is_int
(
$revision
)
)
{
throw
new
LogicException
(
"Trying to load a text for {$this->getPageIdentity()} which is not marked for translation"
);
}
$this
->
revision
=
$revision
;
}
$flags
=
Utilities
::
shouldReadFromPrimary
()
?
IDBAccessObject
::
READ_LATEST
:
IDBAccessObject
::
READ_NORMAL
;
$rev
=
MediaWikiServices
::
getInstance
()
->
getRevisionLookup
()
->
getRevisionByTitle
(
$this
->
getPageIdentity
(),
$this
->
revision
,
$flags
);
$content
=
$rev
->
getContent
(
SlotRecord
::
MAIN
);
$text
=
(
$content
instanceof
TextContent
)
?
$content
->
getText
()
:
null
;
if
(
!
is_string
(
$text
)
)
{
throw
new
RuntimeException
(
"Failed to load text for {$this->getPageIdentity()}"
);
}
$this
->
text
=
$text
;
return
$this
->
text
;
}
/**
* Revision is null if object was constructed using newFromText.
* @return null|int
*/
public
function
getRevision
():
?
int
{
return
$this
->
revision
;
}
/**
* Returns the source language of this translatable page. In other words
* the language in which the page without language code is written.
* @since 2013-01-28
*/
public
function
getSourceLanguageCode
():
string
{
return
$this
->
getTitle
()->
getPageLanguage
()->
getCode
();
}
/** @inheritDoc */
public
function
getMessageGroupId
():
string
{
return
self
::
getMessageGroupIdFromTitle
(
$this
->
getPageIdentity
()
);
}
/** Constructs MessageGroup id for any title. */
public
static
function
getMessageGroupIdFromTitle
(
PageReference
$page
):
string
{
return
'page-'
.
MediaWikiServices
::
getInstance
()->
getTitleFormatter
()->
getPrefixedText
(
$page
);
}
/**
* Returns MessageGroup used for translating this page. It may still be empty
* if the page has not been ever marked.
*/
public
function
getMessageGroup
():
?
WikiPageMessageGroup
{
$groupId
=
$this
->
getMessageGroupId
();
$group
=
MessageGroups
::
getGroup
(
$groupId
);
if
(
!
$group
||
$group
instanceof
WikiPageMessageGroup
)
{
return
$group
;
}
throw
new
RuntimeException
(
"Expected $groupId to be of type WikiPageMessageGroup; got "
.
get_class
(
$group
)
);
}
/** Check whether title is marked for translation */
public
function
hasPageDisplayTitle
():
bool
{
// Cached value
if
(
$this
->
pageDisplayTitle
!==
null
)
{
return
$this
->
pageDisplayTitle
;
}
// Check if title section exists in list of sections
$factory
=
Services
::
getInstance
()->
getTranslationUnitStoreFactory
();
$store
=
$factory
->
getReader
(
$this
->
getPageIdentity
()
);
$this
->
pageDisplayTitle
=
in_array
(
self
::
DISPLAY_TITLE_UNIT_ID
,
$store
->
getNames
()
);
return
$this
->
pageDisplayTitle
;
}
/** Get translated page title. */
public
function
getPageDisplayTitle
(
string
$languageCode
):
?
string
{
// Return null if title not marked for translation
if
(
!
$this
->
hasPageDisplayTitle
()
)
{
return
null
;
}
// Display title from DB
$section
=
str_replace
(
' '
,
'_'
,
self
::
DISPLAY_TITLE_UNIT_ID
);
$page
=
MediaWikiServices
::
getInstance
()->
getTitleFormatter
()->
getPrefixedDBkey
(
$this
->
getPageIdentity
()
);
try
{
$group
=
$this
->
getMessageGroup
();
}
catch
(
RuntimeException
$e
)
{
return
null
;
}
// Sanity check, seems to happen during moves
if
(
!
$group
)
{
return
null
;
}
return
$group
->
getMessage
(
"$page/$section"
,
$languageCode
,
IDBAccessObject
::
READ_NORMAL
);
}
public
function
getStrippedSourcePageText
():
string
{
$parser
=
Services
::
getInstance
()->
getTranslatablePageParser
();
$text
=
$parser
->
cleanupTags
(
$this
->
getText
()
);
$text
=
preg_replace
(
'~<languages
\s
*/>
\n
?~s'
,
''
,
$text
);
return
$text
;
}
public
static
function
getTranslationPageFromTitle
(
Title
$title
):
?
TranslationPage
{
$self
=
self
::
isTranslationPage
(
$title
);
return
$self
?
$self
->
getTranslationPage
(
$self
->
targetLanguage
)
:
null
;
}
public
function
getTranslationPage
(
string
$targetLanguage
):
TranslationPage
{
$mwServices
=
MediaWikiServices
::
getInstance
();
$config
=
$mwServices
->
getMainConfig
();
$services
=
Services
::
getInstance
();
$parser
=
$services
->
getTranslatablePageParser
();
$parserOutput
=
$parser
->
parse
(
$this
->
getText
()
);
$pageVersion
=
(
int
)
$services
->
getMessageGroupMetadata
()
->
get
(
$this
->
getMessageGroupId
(),
'version'
);
$wrapUntranslated
=
$pageVersion
>=
2
;
$languageFactory
=
$mwServices
->
getLanguageFactory
();
return
new
TranslationPage
(
$parserOutput
,
$this
->
getMessageGroup
(),
$languageFactory
->
getLanguage
(
$targetLanguage
),
$languageFactory
->
getLanguage
(
$this
->
getSourceLanguageCode
()
),
$config
->
get
(
'TranslateKeepOutdatedTranslations'
),
$wrapUntranslated
,
$this
->
getTitle
()
);
}
/** Adds a tag which indicates that this page is suitable for translation. */
public
function
addMarkedTag
(
int
$revision
,
?
array
$value
=
null
)
{
$this
->
revTagStore
->
replaceTag
(
$this
->
getPageIdentity
(),
RevTagStore
::
TP_MARK_TAG
,
$revision
,
$value
);
self
::
clearSourcePageCache
();
}
/** Adds a tag which indicates that this page source is ready for marking for translation. */
public
function
addReadyTag
(
int
$revision
):
void
{
$this
->
revTagStore
->
replaceTag
(
$this
->
getPageIdentity
(),
RevTagStore
::
TP_READY_TAG
,
$revision
);
if
(
!
self
::
isSourcePage
(
$this
->
getPageIdentity
()
)
)
{
self
::
clearSourcePageCache
();
}
}
/** Returns the latest revision which has marked tag, if any. */
public
function
getMarkedTag
():
?
int
{
return
$this
->
revTagStore
->
getLatestRevisionWithTag
(
$this
->
getPageIdentity
(),
RevTagStore
::
TP_MARK_TAG
);
}
/** Returns the latest revision which has ready tag, if any. */
public
function
getReadyTag
():
?
int
{
return
$this
->
revTagStore
->
getLatestRevisionWithTag
(
$this
->
getPageIdentity
(),
RevTagStore
::
TP_READY_TAG
);
}
/**
* Produces a link to translation view of a translation page.
* @param string|bool $code MediaWiki language code. Default: false.
* @return string Relative url
*/
public
function
getTranslationUrl
(
$code
=
false
):
string
{
$params
=
[
'group'
=>
$this
->
getMessageGroupId
(),
'action'
=>
'page'
,
'filter'
=>
''
,
'language'
=>
$code
,
];
$translate
=
SpecialPage
::
getTitleFor
(
'Translate'
);
return
$translate
->
getLocalURL
(
$params
);
}
public
function
getMarkedRevs
():
IResultWrapper
{
$db
=
Utilities
::
getSafeReadDB
();
return
$db
->
newSelectQueryBuilder
()
->
select
(
[
'rt_revision'
,
'rt_value'
]
)
->
from
(
'revtag'
)
->
where
(
[
'rt_page'
=>
$this
->
getPageIdentity
()->
getId
(),
'rt_type'
=>
RevTagStore
::
TP_MARK_TAG
,
]
)
->
orderBy
(
'rt_revision'
,
SelectQueryBuilder
::
SORT_DESC
)
->
caller
(
__METHOD__
)
->
fetchResultSet
();
}
/** @inheritDoc */
public
function
getTranslationPages
():
array
{
$mwServices
=
MediaWikiServices
::
getInstance
();
$messageGroup
=
$this
->
getMessageGroup
();
$knownLanguageCodes
=
$messageGroup
?
$messageGroup
->
getTranslatableLanguages
()
:
null
;
$knownLanguageCodes
??=
Utilities
::
getLanguageNames
(
LanguageNameUtils
::
AUTONYMS
);
$prefixedDbTitleKey
=
$this
->
getPageIdentity
()->
getDBkey
()
.
'/'
;
$baseNamespace
=
$this
->
getPageIdentity
()->
getNamespace
();
// Build a link batch query for all translation pages
$linkBatch
=
$mwServices
->
getLinkBatchFactory
()->
newLinkBatch
();
foreach
(
array_keys
(
$knownLanguageCodes
)
as
$code
)
{
$linkBatch
->
add
(
$baseNamespace
,
$prefixedDbTitleKey
.
$code
);
}
$translationPages
=
[];
foreach
(
$linkBatch
->
getPageIdentities
()
as
$pageIdentity
)
{
if
(
$pageIdentity
->
exists
()
)
{
$translationPages
[]
=
Title
::
castFromPageIdentity
(
$pageIdentity
);
}
}
return
$translationPages
;
}
/** @inheritDoc */
public
function
getTranslationUnitPages
(
?
string
$code
=
null
):
array
{
return
$this
->
getTranslationUnitPagesByTitle
(
$this
->
title
,
$code
);
}
public
function
getTranslationPercentages
():
array
{
// Calculate percentages for the available translations
try
{
$group
=
$this
->
getMessageGroup
();
}
catch
(
RuntimeException
$e
)
{
return
[];
}
if
(
!
$group
)
{
return
[];
}
$titles
=
$this
->
getTranslationPages
();
$temp
=
MessageGroupStats
::
forGroup
(
$this
->
getMessageGroupId
()
);
$stats
=
[];
foreach
(
$titles
as
$t
)
{
$handle
=
new
MessageHandle
(
$t
);
$code
=
$handle
->
getCode
();
// Sometimes we want to display 0.00 for pages for which translation
// hasn't started yet.
$stats
[
$code
]
=
0.00
;
if
(
(
$temp
[
$code
][
MessageGroupStats
::
TOTAL
]
??
0
)
>
0
)
{
$total
=
$temp
[
$code
][
MessageGroupStats
::
TOTAL
];
$translated
=
$temp
[
$code
][
MessageGroupStats
::
TRANSLATED
];
$percentage
=
$translated
/
$total
;
$stats
[
$code
]
=
sprintf
(
'%.2f'
,
$percentage
);
}
}
// Content language is always up-to-date
$stats
[
$this
->
getSourceLanguageCode
()]
=
1.00
;
return
$stats
;
}
public
function
getTransRev
(
string
$suffix
)
{
$title
=
Title
::
makeTitle
(
NS_TRANSLATIONS
,
$suffix
);
$db
=
Utilities
::
getSafeReadDB
();
return
$db
->
newSelectQueryBuilder
()
->
select
(
'rt_value'
)
->
from
(
'revtag'
)
->
where
(
[
'rt_page'
=>
$title
->
getArticleID
(),
'rt_type'
=>
RevTagStore
::
TRANSVER_PROP
,
]
)
->
orderBy
(
'rt_revision'
,
SelectQueryBuilder
::
SORT_DESC
)
->
caller
(
__METHOD__
)
->
fetchField
();
}
public
function
supportsTransclusion
():
?
bool
{
$transclusion
=
Services
::
getInstance
()
->
getMessageGroupMetadata
()
->
get
(
$this
->
getMessageGroupId
(),
'transclusion'
);
if
(
$transclusion
===
false
)
{
return
null
;
}
return
$transclusion
===
'1'
;
}
public
function
getRevisionRecordWithFallback
():
?
RevisionRecord
{
$title
=
$this
->
getTitle
();
$store
=
MediaWikiServices
::
getInstance
()->
getRevisionStore
();
$revRecord
=
$store
->
getRevisionByTitle
(
$title
->
getSubpage
(
$this
->
targetLanguage
)
);
if
(
$revRecord
)
{
return
$revRecord
;
}
// Fetch the source fallback
return
$store
->
getRevisionByTitle
(
$title
->
getSubpage
(
$this
->
getSourceLanguageCode
()
)
);
}
/** @inheritDoc */
public
function
isMoveable
():
bool
{
return
$this
->
getMarkedTag
()
!==
null
;
}
/** @inheritDoc */
public
function
isDeletable
():
bool
{
return
$this
->
getMarkedTag
()
!==
null
;
}
/** @return bool|self */
public
static
function
isTranslationPage
(
Title
$title
)
{
$handle
=
new
MessageHandle
(
$title
);
if
(
!
Utilities
::
isTranslationPage
(
$handle
)
)
{
return
false
;
}
$languageCode
=
$handle
->
getCode
();
$newTitle
=
$handle
->
getTitleForBase
();
$page
=
self
::
newFromTitle
(
$newTitle
);
if
(
$page
->
getMarkedTag
()
===
null
)
{
return
false
;
}
$page
->
targetLanguage
=
$languageCode
;
return
$page
;
}
/** Helper to guess translation page from translation unit. */
public
static
function
parseTranslationUnit
(
LinkTarget
$translationUnit
):
array
{
// Format is Translations:SourcePageNamespace:SourcePageName/SectionName/LanguageCode.
// We will drop the namespace immediately here.
$parts
=
explode
(
'/'
,
$translationUnit
->
getText
()
);
// LanguageCode and SectionName are guaranteed to not have '/'.
$language
=
array_pop
(
$parts
);
$section
=
array_pop
(
$parts
);
$sourcepage
=
implode
(
'/'
,
$parts
);
return
[
'sourcepage'
=>
$sourcepage
,
'section'
=>
$section
,
'language'
=>
$language
];
}
public
static
function
isSourcePage
(
PageIdentity
$page
):
bool
{
if
(
!
$page
->
exists
()
)
{
// No point in loading all translatable pages if the page
// doesn’t exist. This also avoids PreconditionExceptions
// if $page is a Title pointing to a non-proper page like
// a special page.
return
false
;
}
$localCache
=
MediaWikiServices
::
getInstance
()->
getLocalServerObjectCache
();
$localKey
=
$localCache
->
makeKey
(
'pagetranslation'
,
'sourcepages'
,
'local'
);
// Store the value in the local cache for a short duration to reduce the number of
// times we hit the WAN cache. See: T366455
$translatablePageIds
=
$localCache
->
getWithSetCallback
(
$localKey
,
$localCache
::
TTL_SECOND
*
8
,
static
function
()
{
$cache
=
MediaWikiServices
::
getInstance
()->
getMainWANObjectCache
();
$cacheKey
=
$cache
->
makeKey
(
'pagetranslation'
,
'sourcepages'
);
return
$cache
->
getWithSetCallback
(
$cacheKey
,
$cache
::
TTL_HOUR
*
2
,
[
TranslatablePage
::
class
,
'getCacheValue'
],
[
'checkKeys'
=>
[
$cacheKey
],
'pcTTL'
=>
$cache
::
TTL_PROC_SHORT
,
'pcGroup'
=>
__CLASS__
.
':1'
,
'version'
=>
3
,
]
);
},
$localCache
::
READ_LATEST
);
return
str_contains
(
$translatablePageIds
,
(
','
.
$page
->
getId
()
.
','
)
);
}
/** Clears the source page cache */
public
static
function
clearSourcePageCache
():
void
{
$cache
=
MediaWikiServices
::
getInstance
()->
getMainWANObjectCache
();
$cache
->
touchCheckKey
(
$cache
->
makeKey
(
'pagetranslation'
,
'sourcepages'
)
);
}
public
static
function
determineStatus
(
?
int
$readyRevisionId
,
?
int
$markRevisionId
,
int
$latestRevisionId
):
?
TranslatablePageStatus
{
$status
=
null
;
if
(
$markRevisionId
===
null
)
{
// Never marked, check that the latest version is ready
if
(
$readyRevisionId
===
$latestRevisionId
)
{
$status
=
TranslatablePageStatus
::
PROPOSED
;
}
else
{
// Otherwise, ignore such pages
return
null
;
}
}
elseif
(
$readyRevisionId
===
$latestRevisionId
)
{
if
(
$markRevisionId
===
$readyRevisionId
)
{
// Marked and latest version is fine
$status
=
TranslatablePageStatus
::
ACTIVE
;
}
else
{
$status
=
TranslatablePageStatus
::
OUTDATED
;
}
}
else
{
// Marked but latest version is not fine
$status
=
TranslatablePageStatus
::
BROKEN
;
}
return
new
TranslatablePageStatus
(
$status
);
}
/**
* Get list of translatable page ids to be stored in the cache
* @internal
* @param mixed $oldValue
* @param int &$ttl
* @param array &$setOpts
* @return string
*/
public
static
function
getCacheValue
(
$oldValue
,
&
$ttl
,
array
&
$setOpts
):
string
{
$dbr
=
MediaWikiServices
::
getInstance
()->
getConnectionProvider
()->
getReplicaDatabase
();
$setOpts
+=
Database
::
getCacheSetOptions
(
$dbr
);
$ids
=
RevTagStore
::
getTranslatableBundleIds
(
RevTagStore
::
TP_MARK_TAG
,
RevTagStore
::
TP_READY_TAG
);
// Adding a comma at the end and beginning so that we can check for page Id
// existence with the "," delimiters
return
','
.
implode
(
','
,
$ids
)
.
','
;
}
}
class_alias
(
TranslatablePage
::
class
,
'TranslatablePage'
);
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Oct 3 2026, 00:12 (5 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
b4/42/2e6ce00df749141eb08d71b351b7
Default Alt Text
TranslatablePage.php (17 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment