summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'MLEB/Translate/tag')
-rw-r--r--MLEB/Translate/tag/PageTranslationHooks.php456
-rw-r--r--MLEB/Translate/tag/SpecialPageMigration.php12
-rw-r--r--MLEB/Translate/tag/SpecialPagePreparation.php2
-rw-r--r--MLEB/Translate/tag/SpecialPageTranslation.php527
-rw-r--r--MLEB/Translate/tag/SpecialPageTranslationDeletePage.php184
-rw-r--r--MLEB/Translate/tag/SpecialPageTranslationMovePage.php209
-rw-r--r--MLEB/Translate/tag/TPParse.php27
-rw-r--r--MLEB/Translate/tag/TPSection.php31
-rw-r--r--MLEB/Translate/tag/TranslatablePage.php197
-rw-r--r--MLEB/Translate/tag/TranslatablePageMoveJob.php14
-rw-r--r--MLEB/Translate/tag/TranslateDeleteJob.php34
-rw-r--r--MLEB/Translate/tag/TranslateMoveJob.php221
-rw-r--r--MLEB/Translate/tag/TranslateRenderJob.php26
-rw-r--r--MLEB/Translate/tag/TranslationsUpdateJob.php37
14 files changed, 1054 insertions, 923 deletions
diff --git a/MLEB/Translate/tag/PageTranslationHooks.php b/MLEB/Translate/tag/PageTranslationHooks.php
index 76d03c8c..205987f5 100644
--- a/MLEB/Translate/tag/PageTranslationHooks.php
+++ b/MLEB/Translate/tag/PageTranslationHooks.php
@@ -7,6 +7,12 @@
* @license GPL-2.0-or-later
*/
+use MediaWiki\Extensions\Translate\SystemUsers\FuzzyBot;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\User\UserIdentity;
use Wikimedia\ScopedCallback;
/**
@@ -28,16 +34,17 @@ class PageTranslationHooks {
private static $languageLinkData = [];
/**
- * Hook: ParserBeforeStrip
+ * Hook: ParserBeforeInternalParse
* @param Parser $parser
* @param string &$text
+ * @param-taint $text escapes_htmlnoent
* @param string $state
* @return bool
*/
public static function renderTagPage( $parser, &$text, $state ) {
$title = $parser->getTitle();
- if ( strpos( $text, '<translate>' ) !== false ) {
+ if ( preg_match( '~</?translate[ >]~', $text ) !== 0 ) {
try {
$parse = TranslatablePage::newFromText( $parser->getTitle(), $text )->getParse();
$text = $parse->getTranslationPageText( null );
@@ -60,10 +67,11 @@ class PageTranslationHooks {
}
self::$renderingContext = true;
- list( , $code ) = TranslateUtils::figureMessage( $title->getText() );
+ [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
$name = $page->getPageDisplayTitle( $code );
if ( $name ) {
$name = $parser->recursivePreprocess( $name );
+ $name = $parser->getTargetLanguage()->convert( $name );
$parser->getOutput()->setDisplayTitle( $name );
}
self::$renderingContext = false;
@@ -79,9 +87,6 @@ class PageTranslationHooks {
// Disable edit section links
$parser->getOutput()->setExtensionData( 'Translate-noeditsection', true );
- if ( !defined( 'ParserOutput::SUPPORTS_STATELESS_TRANSFORMS' ) ) {
- $parser->getOptions()->setEditSection( false );
- }
return true;
}
@@ -105,14 +110,14 @@ class PageTranslationHooks {
* Hook: PageContentLanguage
*
* @param Title $title
- * @param Language &$pageLang
+ * @param Language|StubUserLang|string &$pageLang
* @return true
*/
public static function onPageContentLanguage( Title $title, &$pageLang ) {
// For translation pages, parse plural, grammar etc with correct language,
// and set the right direction
if ( TranslatablePage::isTranslationPage( $title ) ) {
- list( , $code ) = TranslateUtils::figureMessage( $title->getText() );
+ [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
$pageLang = Language::factory( $code );
}
@@ -127,21 +132,33 @@ class PageTranslationHooks {
* @param int $oldid
* @param array &$notices
*/
- public static function onTitleGetEditNotices( Title $title, $oldid, array &$notices ) {
- $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage();
+ public static function onTitleGetEditNotices( Title $title, int $oldid, array &$notices ) {
+ if ( TranslatablePage::isSourcePage( $title ) ) {
+ $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage();
+ if ( !$msg->isDisabled() ) {
+ $notices['translate-tag'] = $msg->parseAsBlock();
+ }
+
+ $label = wfMessage( 'tps-edit-sourcepage-title' )->escaped();
+ $msg = Html::rawElement(
+ 'div',
+ [],
+ wfMessage( 'tps-edit-sourcepage-text' )->parse()
+ );
- if ( !$msg->isDisabled() && TranslatablePage::isSourcePage( $title ) ) {
- $notices['translate-tag'] = $msg->parseAsBlock();
+ $notices[] = TranslateUtils::fieldset(
+ $label, $msg, [ 'class' => 'mw-infobox translate-edit-documentation' ]
+ );
}
}
/**
- * Hook: OutputPageBeforeHTML
+ * Hook: BeforePageDisplay
* @param OutputPage $out
- * @param string $text
+ * @param Skin $skin
* @return true
*/
- public static function injectCss( OutputPage $out, /*string*/$text ) {
+ public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
global $wgTranslatePageTranslationULS;
$title = $out->getTitle();
@@ -153,6 +170,12 @@ class PageTranslationHooks {
$out->addModules( 'ext.translate.pagetranslation.uls' );
}
+ if ( $isSource && TranslateUtils::isEditPage( $out->getContext()->getRequest() ) ) {
+ // Adding a help notice
+ $out->addModuleStyles( 'ext.translate.edit.documentation.styles' );
+ $out->addModules( 'ext.translate.edit.documentation' );
+ }
+
if ( $isTranslation ) {
// Source pages get this module via <translate>, but for translation
// pages we need to add it manually.
@@ -174,15 +197,14 @@ class PageTranslationHooks {
* @param string $summary
* @param bool $minor
* @param int $flags
- * @param Revision $revision
* @param MessageHandle $handle
* @return true
*/
public static function onSectionSave( WikiPage $wikiPage, User $user, TextContent $content,
- $summary, $minor, $flags, $revision, MessageHandle $handle
+ $summary, $minor, $flags, MessageHandle $handle
) {
// FuzzyBot may do some duplicate work already worked on by other jobs
- if ( FuzzyBot::getName() === $user->getName() ) {
+ if ( $user->equals( FuzzyBot::getUser() ) ) {
return true;
}
@@ -197,7 +219,11 @@ class PageTranslationHooks {
// Update the target translation page
if ( !$handle->isDoc() ) {
$code = $handle->getCode();
- self::updateTranslationPage( $page, $code, $user, $flags, $summary );
+ DeferredUpdates::addCallableUpdate(
+ function () use ( $page, $code, $user, $flags, $summary ) {
+ self::updateTranslationPage( $page, $code, $user, $flags, $summary );
+ }
+ );
}
return true;
@@ -294,7 +320,7 @@ class PageTranslationHooks {
$classes = array_merge( $classes, self::tpProgressIcon( $percent ) );
$element = Html::rawElement(
'span',
- [ 'class' => $classes , 'lang' => TranslateUtils::bcp47( $code ) ],
+ [ 'class' => $classes , 'lang' => LanguageCode::bcp47( $code ) ],
$name
);
} elseif ( $subpage->isKnown() ) {
@@ -313,7 +339,7 @@ class PageTranslationHooks {
$attribs = [
'title' => $title,
'class' => $classes,
- 'lang' => TranslateUtils::bcp47( $code ),
+ 'lang' => LanguageCode::bcp47( $code ),
];
$element = Linker::linkKnown( $subpage, $name, $attribs );
@@ -554,8 +580,8 @@ class PageTranslationHooks {
$link[ 'href' ] = $data[ 'href' ];
$link[ 'text' ] = $data[ 'autonym' ];
$link[ 'title' ] = $data[ 'title' ]->inLanguage( $out->getLanguage()->getCode() )->text();
- $link[ 'lang'] = wfBCP47( $data[ 'language' ] );
- $link[ 'hreflang'] = wfBCP47( $data[ 'language' ] );
+ $link[ 'lang'] = LanguageCode::bcp47( $data[ 'language' ] );
+ $link[ 'hreflang'] = LanguageCode::bcp47( $data[ 'language' ] );
$out->addModuleStyles( 'ext.translate.tag.languages' );
}
@@ -570,16 +596,7 @@ class PageTranslationHooks {
* @return true
*/
public static function tpSyntaxCheckForEditContent( $context, $content, $status, $summary ) {
- if ( !$content instanceof TextContent ) {
- return true; // whatever.
- }
-
- $text = $content->getNativeData();
- // See T154500
- $text = str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) );
- $title = $context->getTitle();
-
- $e = self::tpSyntaxError( $title, $text );
+ $e = self::tpSyntaxError( $context->getTitle(), $content );
if ( $e ) {
$msg = $e->getMsg();
@@ -592,25 +609,31 @@ class PageTranslationHooks {
return true;
}
- /**
- * Returns any syntax error.
- * @param Title $title
- * @param string $text
- * @return null|TPException
- */
- protected static function tpSyntaxError( $title, $text ) {
- if ( strpos( $text, '<translate>' ) === false ) {
+ /** Returns any syntax error */
+ protected static function tpSyntaxError( ?Title $title, Content $content ): ?TPException {
+ if ( !$content instanceof TextContent || !$title ) {
+ return null;
+ }
+
+ $text = $content->getNativeData();
+
+ // See T154500
+ $text = str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) );
+
+ if ( preg_match( '~</?translate[ >]~', $text ) === 0 ) {
return null;
}
$page = TranslatablePage::newFromText( $title, $text );
+
+ $exception = null;
try {
$page->getParse();
-
- return null;
} catch ( TPException $e ) {
- return $e;
+ $exception = $e;
}
+
+ return $exception;
}
/**
@@ -631,34 +654,64 @@ class PageTranslationHooks {
public static function tpSyntaxCheck( WikiPage $wikiPage, $user, $content, $summary,
$minor, $_1, $_2, $flags, $status
) {
+ $e = self::tpSyntaxError( $wikiPage->getTitle(), $content );
+ if ( $e ) {
+ call_user_func_array( [ $status, 'fatal' ], $e->getMsg() );
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Hook: PageSaveComplete
+ *
+ * Only run in versions of mediawiki beginning 1.35; before 1.35, ::addTranstag is used
+ *
+ * @param WikiPage $wikiPage
+ * @param UserIdentity $userIdentity
+ * @param string $summary
+ * @param int $flags
+ * @param RevisionRecord $revisionRecord
+ * @param mixed $editResult documented as mixed because the EditResult class didn't exist
+ * before 1.35
+ * @return true
+ */
+ public static function addTranstagAfterSave(
+ WikiPage $wikiPage,
+ UserIdentity $userIdentity,
+ string $summary,
+ int $flags,
+ RevisionRecord $revisionRecord,
+ $editResult
+ ) {
+ $content = $wikiPage->getContent();
+
if ( $content instanceof TextContent ) {
$text = $content->getNativeData();
- // See T154500
- $text = str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) );
} else {
// Screw it, not interested
return true;
}
// Quick escape on normal pages
- if ( strpos( $text, '<translate>' ) === false ) {
+ if ( preg_match( '~</?translate[ >]~', $text ) === 0 ) {
return true;
}
- $page = TranslatablePage::newFromText( $wikiPage->getTitle(), $text );
- try {
- $page->getParse();
- } catch ( TPException $e ) {
- call_user_func_array( [ $status, 'fatal' ], $e->getMsg() );
-
- return false;
- }
+ // Add the ready tag
+ $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
+ $page->addReadyTag( $revisionRecord->getId() );
return true;
}
/**
* Hook: PageContentSaveComplete
+ *
+ * Only run in versions of mediawiki before 1.35; in 1.35+, ::addTranstag is used
+ *
* @param WikiPage $wikiPage
* @param User $user
* @param Content $content
@@ -686,7 +739,7 @@ class PageTranslationHooks {
}
// Quick escape on normal pages
- if ( strpos( $text, '</translate>' ) === false ) {
+ if ( preg_match( '~</?translate[ >]~', $text ) === 0 ) {
return true;
}
@@ -710,47 +763,30 @@ class PageTranslationHooks {
* update the translation pages in the case, the non-text changes affect
* the rendering of translation pages. I'm not aware of any such cases
* at the moment.
- * Hook: RevisionInsertComplete
+ * Hook: RevisionRecordInserted
* @since 2012-05-08
- * @param Revision $rev
- * @param string $text
- * @param int $flags
+ * @param RevisionRecord $rev
* @return true
*/
- public static function updateTranstagOnNullRevisions( Revision $rev, $text, $flags ) {
- $title = $rev->getTitle();
-
- $newRevId = $rev->getId();
- $oldRevId = $rev->getParentId();
- $newTextId = $rev->getTextId();
+ public static function updateTranstagOnNullRevisions( RevisionRecord $rev ) {
+ $prevRev = MediaWikiServices::getInstance()->getRevisionLookup()
+ ->getPreviousRevision( $rev );
- /* This hook doesn't provide any way to detech null revisions
- * without extra query */
- $dbw = wfGetDB( DB_MASTER );
- $table = 'revision';
- $field = 'rev_text_id';
- $conds = [
- 'rev_page' => $rev->getPage(),
- 'rev_id' => $oldRevId,
- ];
- // FIXME: optimize away this query. Bug T38588.
- $oldTextId = $dbw->selectField( $table, $field, $conds, __METHOD__ );
-
- if ( (string)$newTextId !== (string)$oldTextId ) {
+ if ( !$prevRev || $prevRev->getSha1() !== $rev->getSha1() ) {
// Not a null revision, bail out.
return true;
}
+ $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
$page = TranslatablePage::newFromTitle( $title );
- if ( $page->getReadyTag() === $oldRevId ) {
- $page->addReadyTag( $newRevId );
+ if ( $page->getReadyTag() === $prevRev->getId() ) {
+ $page->addReadyTag( $rev->getId() );
}
-
return true;
}
/**
- * Prevent editing of certain pages in Translations namespace.
+ * Prevent creation of orphan translation units in Translations namespace.
* Hook: getUserPermissionsErrorsExpensive
*
* @param Title $title
@@ -759,38 +795,84 @@ class PageTranslationHooks {
* @param mixed &$result
* @return bool
*/
- public static function onGetUserPermissionsErrorsExpensive( Title $title, User $user,
- $action, &$result
+ public static function onGetUserPermissionsErrorsExpensive(
+ Title $title, User $user, $action, &$result
) {
$handle = new MessageHandle( $title );
- // Check only when someone tries to edit (or create) page translation messages
- if ( $action !== 'edit' || !$handle->isPageTranslation() ) {
+ // Check only when someone tries to create translation units.
+ // Allow editing units that become orphaned in regular use, so that
+ // people can delete them or fix links or other issues in them.
+ if ( $action !== 'create' || !$handle->isPageTranslation() ) {
return true;
}
- if ( !$handle->isValid() ) {
- // Don't allow editing invalid messages that do not belong to any translatable page
- $result = [ 'tpt-unknown-page' ];
- return false;
+ $isValid = true;
+ $groupId = null;
+
+ if ( $handle->isValid() ) {
+ $groupId = $handle->getGroup()->getId();
+ } else {
+ // Sometimes the message index can be out of date. Either the rebuild job failed or
+ // it just hasn't finished yet. Do a secondary check to make sure we are not
+ // inconveniencing translators for no good reason.
+ // See https://phabricator.wikimedia.org/T221119
+ MediaWikiServices::getInstance()->getStatsdDataFactory()
+ ->increment( 'translate.slow_translatable_page_check' );
+ $translatablePage = self::checkTranslatablePageSlow( $title );
+ if ( $translatablePage ) {
+ $groupId = $translatablePage->getMessageGroupId();
+ } else {
+ $isValid = false;
+ }
}
- $error = self::getTranslationRestrictions( $handle );
- if ( count( $error ) ) {
- $result = $error;
- return false;
+ if ( $isValid ) {
+ $error = self::getTranslationRestrictions( $handle, $groupId );
+ $result = $error ?: $result;
+ return $error === [];
}
- return true;
+ // Don't allow editing invalid messages that do not belong to any translatable page
+ LoggerFactory::getInstance( 'Translate' )->info(
+ 'Unknown translation page: {title}',
+ [ 'title' => $title->getPrefixedDBkey() ]
+ );
+ $result = [ 'tpt-unknown-page' ];
+ return false;
+ }
+
+ private static function checkTranslatablePageSlow( LinkTarget $unit ) : ?TranslatablePage {
+ $parts = TranslatablePage::parseTranslationUnit( $unit );
+ $translationPageTitle = Title::newFromText(
+ $parts[ 'sourcepage' ] . '/' . $parts[ 'language' ]
+ );
+ if ( !$translationPageTitle ) {
+ return null;
+ }
+
+ $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle );
+ if ( !$translatablePage ) {
+ return null;
+ }
+
+ $sections = $translatablePage->getSections();
+
+ if ( !in_array( $parts[ 'section' ], $sections ) ) {
+ return null;
+ }
+
+ return $translatablePage;
}
/**
* Prevent editing of restricted languages when prioritized.
*
* @param MessageHandle $handle
+ * @param string $groupId
* @return array array containing error message if restricted, empty otherwise
*/
- private static function getTranslationRestrictions( MessageHandle $handle ) {
+ private static function getTranslationRestrictions( MessageHandle $handle, $groupId ) {
global $wgTranslateDocumentationLanguageCode;
// Allow adding message documentation even when translation is restricted
@@ -798,10 +880,6 @@ class PageTranslationHooks {
return [];
}
- // Get the primary group id
- $ids = $handle->getGroupIds();
- $groupId = $ids[0];
-
// Check if anything is prevented for the group in the first place
$force = TranslateMetadata::get( $groupId, 'priorityforce' );
if ( $force !== 'on' ) {
@@ -837,6 +915,7 @@ class PageTranslationHooks {
$whitelist = [
'read', 'delete', 'undelete', 'deletedtext', 'deletedhistory',
'review', // FlaggedRevs
+ 'patrol', // T151172
];
if ( in_array( $action, $whitelist ) ) {
return true;
@@ -844,7 +923,7 @@ class PageTranslationHooks {
$page = TranslatablePage::isTranslationPage( $title );
if ( $page !== false && $page->getMarkedTag() ) {
- list( , $code ) = TranslateUtils::figureMessage( $title->getText() );
+ [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
$result = [
'tpt-target-page',
':' . $page->getTitle()->getPrefixedText(),
@@ -859,32 +938,6 @@ class PageTranslationHooks {
}
/**
- * Prevent patrol links from appearing on translation pages.
- * Hook: getUserPermissionsErrors
- *
- * @param Title $title
- * @param User $user
- * @param string $action
- * @param mixed &$result
- *
- * @return bool
- */
- public static function preventPatrolling( Title $title, User $user, $action, &$result ) {
- if ( $action !== 'patrol' ) {
- return true;
- }
-
- $page = TranslatablePage::isTranslationPage( $title );
-
- if ( $page !== false ) {
- $result = [ 'tpt-patrolling-blocked' ];
- return false;
- }
-
- return true;
- }
-
- /**
* Redirects the delete action to our own for translatable pages.
* Hook: ArticleConfirmDelete
*
@@ -915,12 +968,12 @@ class PageTranslationHooks {
/**
* Hook: ArticleViewHeader
*
- * @param Article &$article
+ * @param Article $article
* @param bool &$outputDone
* @param bool &$pcache
* @return bool
*/
- public static function translatablePageHeader( &$article, &$outputDone, &$pcache ) {
+ public static function translatablePageHeader( $article, &$outputDone, &$pcache ) {
if ( $article->getOldID() ) {
return true;
}
@@ -986,7 +1039,7 @@ class PageTranslationHooks {
'lang' => $language->getHtmlCode(),
],
$language->semicolonList( $actions )
- ) . Html::element( 'hr' );
+ );
$context->getOutput()->addHTML( $header );
}
@@ -1017,7 +1070,7 @@ class PageTranslationHooks {
return;
}
- list( , $code ) = TranslateUtils::figureMessage( $title->getText() );
+ [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
// Get the translation percentage
$pers = $page->getTranslationPercentages();
@@ -1049,7 +1102,7 @@ class PageTranslationHooks {
'lang' => $language->getHtmlCode(),
],
$msg
- ) . Html::element( 'hr' );
+ );
$output->addHTML( $header );
@@ -1103,11 +1156,11 @@ class PageTranslationHooks {
/**
* Hook: SkinSubPageSubtitle
* @param array &$subpages
- * @param Skin|null $skin
+ * @param ?Skin $skin
* @param OutputPage $out
* @return bool
*/
- public static function replaceSubtitle( &$subpages, Skin $skin = null, OutputPage $out ) {
+ public static function replaceSubtitle( &$subpages, ?Skin $skin, OutputPage $out ) {
$isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
if ( !$isTranslationPage
&& !TranslatablePage::isSourcePage( $out->getTitle() )
@@ -1196,7 +1249,78 @@ class PageTranslationHooks {
/**
* Hook to update source and destination translation pages on moving translation units
+ * Hook: PageMoveComplete
+ *
+ * Only run in versions of mediawiki beginning 1.35; before 1.35, ::onMoveTranslationUnits is used
+ *
+ * @param LinkTarget $oldLinkTarget
+ * @param LinkTarget $newLinkTarget
+ * @param UserIdentity $userIdentity
+ * @param int $oldid
+ * @param int $newid
+ * @param string $reason
+ * @param RevisionRecord $revisionRecord
+ */
+ public static function onMovePageTranslationUnits(
+ LinkTarget $oldLinkTarget,
+ LinkTarget $newLinkTarget,
+ UserIdentity $userIdentity,
+ int $oldid,
+ int $newid,
+ string $reason,
+ RevisionRecord $revisionRecord
+ ) {
+ $user = User::newFromIdentity( $userIdentity );
+ // TranslatablePageMoveJob takes care of handling updates because it performs
+ // a lot of moves at once. As a performance optimization, skip this hook if
+ // we detect moves from that job. As there isn't a good way to pass information
+ // to this hook what originated the move, we use some heuristics.
+ if ( defined( 'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
+ return;
+ }
+
+ $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
+ $newTitle = Title::newFromLinkTarget( $newLinkTarget );
+ $groupLast = null;
+ foreach ( [ $oldTitle, $newTitle ] as $title ) {
+ $handle = new MessageHandle( $title );
+ if ( !$handle->isValid() ) {
+ continue;
+ }
+
+ // Documentation pages are never translation pages
+ if ( $handle->isDoc() ) {
+ continue;
+ }
+
+ $group = $handle->getGroup();
+ if ( !$group instanceof WikiPageMessageGroup ) {
+ continue;
+ }
+
+ $language = $handle->getCode();
+
+ // Ignore pages such as Translations:Page/unit without language code
+ if ( (string)$language === '' ) {
+ continue;
+ }
+
+ // Update the page only once if source and destination units
+ // belong to the same page
+ if ( $group !== $groupLast ) {
+ $groupLast = $group;
+ $page = TranslatablePage::newFromTitle( $group->getTitle() );
+ self::updateTranslationPage( $page, $language, $user, 0, $reason );
+ }
+ }
+ }
+
+ /**
+ * Hook to update source and destination translation pages on moving translation units
* Hook: TitleMoveComplete
+ *
+ * Only run in versions of mediawiki before 1.35; in 1.35+, ::onMovePageTranslationUnits is used
+ *
* @since 2014.08
* @param Title $ot
* @param Title $nt
@@ -1254,14 +1378,14 @@ class PageTranslationHooks {
* Hook to update translation page on deleting a translation unit
* Hook: ArticleDeleteComplete
* @since 2016.05
- * @param WikiPage &$unit
- * @param User &$user
+ * @param WikiPage $unit
+ * @param User $user
* @param string $reason
* @param int $id
* @param Content $content
* @param ManualLogEntry $logEntry
*/
- public static function onDeleteTranslationUnit( WikiPage &$unit, User &$user, $reason,
+ public static function onDeleteTranslationUnit( WikiPage $unit, User $user, $reason,
$id, $content, $logEntry
) {
// Do the update. In case job queue is doing the work, the update is not done here
@@ -1292,36 +1416,44 @@ class PageTranslationHooks {
$langCode = $handle->getCode();
$targetPage = $target->getSubpage( $langCode )->getPrefixedText();
- if ( !isset( $queuedPages[ $targetPage ] ) ) {
- $queuedPages[ $targetPage ] = true;
- $fname = __METHOD__;
+ if ( isset( $queuedPages[ $targetPage ] ) ) {
+ return;
+ }
- $dbw = wfGetDB( DB_MASTER );
- $dbw->onTransactionIdle( function () use ( $dbw, $queuedPages, $targetPage,
- $target, $handle, $langCode, $user, $reason, $fname
- ) {
- $dbw->startAtomic( $fname );
+ $queuedPages[ $targetPage ] = true;
+ $fname = __METHOD__;
- $page = TranslatablePage::newFromTitle( $target );
+ $dbw = wfGetDB( DB_MASTER );
+ $callback = function () use (
+ $dbw, $queuedPages, $targetPage, $target, $handle, $langCode, $user, $reason, $fname
+ ) {
+ $dbw->startAtomic( $fname );
- MessageGroupStats::forItem(
- $page->getMessageGroupId(),
- $langCode,
- MessageGroupStats::FLAG_NO_CACHE
- );
+ $page = TranslatablePage::newFromTitle( $target );
- if ( !$handle->isDoc() ) {
- // Assume that $user and $reason for the first deletion is the same for all
- self::updateTranslationPage( $page, $langCode, $user, 0, $reason );
- }
+ MessageGroupStats::forItem(
+ $page->getMessageGroupId(),
+ $langCode,
+ MessageGroupStats::FLAG_NO_CACHE
+ );
+
+ if ( !$handle->isDoc() ) {
+ // Assume that $user and $reason for the first deletion is the same for all
+ self::updateTranslationPage( $page, $langCode, $user, 0, $reason );
+ }
- // If a unit was deleted after the edit here is done, this allows us
- // to add the page back to the queue again and so we can make another
- // edit here with the latest changes.
- unset( $queuedPages[ $targetPage ] );
+ // If a unit was deleted after the edit here is done, this allows us
+ // to add the page back to the queue again and so we can make another
+ // edit here with the latest changes.
+ unset( $queuedPages[ $targetPage ] );
- $dbw->endAtomic( $fname );
- } );
+ $dbw->endAtomic( $fname );
+ };
+
+ if ( is_callable( [ $dbw, 'onTransactionCommitOrIdle' ] ) ) {
+ $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ );
+ } else {
+ $dbw->onTransactionIdle( $callback, __METHOD__ );
}
}
}
diff --git a/MLEB/Translate/tag/SpecialPageMigration.php b/MLEB/Translate/tag/SpecialPageMigration.php
index a0d1321b..462f0993 100644
--- a/MLEB/Translate/tag/SpecialPageMigration.php
+++ b/MLEB/Translate/tag/SpecialPageMigration.php
@@ -14,7 +14,7 @@ class SpecialPageMigration extends SpecialPage {
}
protected function getGroupName() {
- return 'wiki';
+ return 'translation';
}
public function getDescription() {
@@ -22,7 +22,6 @@ class SpecialPageMigration extends SpecialPage {
}
public function execute( $par ) {
- $request = $this->getRequest();
$output = $this->getOutput();
$this->setHeaders();
$this->checkPermissions();
@@ -32,8 +31,7 @@ class SpecialPageMigration extends SpecialPage {
'ext.translate.special.pagemigration.styles',
'jquery.uls.grid'
] );
- # Get request data from, e.g.
- $param = $request->getText( 'param' );
+
# Do stuff
# ...
$out = '';
@@ -44,12 +42,14 @@ class SpecialPageMigration extends SpecialPage {
[ 'class' => 'mw-tpm-sp-error__message five columns hide' ] );
$out .= Html::closeElement( 'div' );
$out .= Html::openElement( 'form', [ 'class' => 'mw-tpm-sp-form row',
- 'id' => 'mw-tpm-sp-primary-form' ] );
+ 'id' => 'mw-tpm-sp-primary-form', 'action' => '' ] );
$out .= Html::element( 'input', [ 'id' => 'pm-summary', 'type' => 'hidden',
'value' => $this->msg( 'pm-summary-import' )->inContentLanguage()->text() ] );
$out .= "\n";
$out .= Html::element( 'input', [ 'id' => 'title', 'class' => 'mw-searchInput mw-ui-input',
- 'placeholder' => $this->msg( 'pm-pagetitle-placeholder' )->text() ] );
+ 'data-mw-searchsuggest' => FormatJson::encode( [
+ 'wrapAsLink' => false
+ ] ), 'placeholder' => $this->msg( 'pm-pagetitle-placeholder' )->text() ] );
$out .= "\n";
$out .= Html::element( 'input', [ 'id' => 'action-import',
'class' => 'mw-ui-button mw-ui-progressive', 'type' => 'button',
diff --git a/MLEB/Translate/tag/SpecialPagePreparation.php b/MLEB/Translate/tag/SpecialPagePreparation.php
index cd854e06..bdaf5dbf 100644
--- a/MLEB/Translate/tag/SpecialPagePreparation.php
+++ b/MLEB/Translate/tag/SpecialPagePreparation.php
@@ -14,7 +14,7 @@ class SpecialPagePreparation extends SpecialPage {
}
protected function getGroupName() {
- return 'wiki';
+ return 'translation';
}
public function execute( $par ) {
diff --git a/MLEB/Translate/tag/SpecialPageTranslation.php b/MLEB/Translate/tag/SpecialPageTranslation.php
index 98eb5d6a..df9aeb5b 100644
--- a/MLEB/Translate/tag/SpecialPageTranslation.php
+++ b/MLEB/Translate/tag/SpecialPageTranslation.php
@@ -8,6 +8,8 @@
* @license GPL-2.0-or-later
*/
+use MediaWiki\Revision\RevisionRecord;
+
/**
* A special page for marking revisions of pages for translation.
*
@@ -18,6 +20,9 @@
* @ingroup SpecialPage PageTranslation
*/
class SpecialPageTranslation extends SpecialPage {
+ private const LATEST_SYNTAX_VERSION = '1';
+ private const DEFAULT_SYNTAX_VERSION = '1';
+
public function __construct() {
parent::__construct( 'PageTranslation' );
}
@@ -27,7 +32,7 @@ class SpecialPageTranslation extends SpecialPage {
}
protected function getGroupName() {
- return 'pagetools';
+ return 'translation';
}
public function execute( $parameters ) {
@@ -42,6 +47,7 @@ class SpecialPageTranslation extends SpecialPage {
$out = $this->getOutput();
$out->addModules( 'ext.translate.special.pagetranslation' );
$out->addHelpLink( 'Help:Extension:Translate/Page_translation_example' );
+ $out->enableOOUI();
if ( $target === '' ) {
$this->listPages();
@@ -80,7 +86,7 @@ class SpecialPageTranslation extends SpecialPage {
// On GET requests, show form which has token
if ( !$request->wasPosted() ) {
if ( $action === 'unlink' ) {
- $this->showUnlinkConfirmation( $title, $target );
+ $this->showUnlinkConfirmation( $title );
} else {
$params = [
'do' => $action,
@@ -112,11 +118,19 @@ class SpecialPageTranslation extends SpecialPage {
$entry->publish( $logid );
}
- $this->listPages();
-
+ // Defer stats purging of parent aggregate groups. Shared groups can contain other
+ // groups as well, which we do not need to update. We could filter non-aggregate
+ // groups out, or use MessageGroups::getParentGroups, though it has an inconvenient
+ // return value format for this use case.
$group = MessageGroups::getGroup( $id );
- $parents = MessageGroups::getSharedGroups( $group );
- MessageGroupStats::clearGroup( $parents );
+ $sharedGroupIds = MessageGroups::getSharedGroups( $group );
+ if ( $sharedGroupIds !== [] ) {
+ $job = MessageGroupStatsRebuildJob::newRefreshGroupsJob( $sharedGroupIds );
+ JobQueueGroup::singleton()->push( $job );
+ }
+
+ // Show updated page with a notice
+ $this->listPages();
return;
}
@@ -201,16 +215,7 @@ class SpecialPageTranslation extends SpecialPage {
return;
}
- $lastrev = $page->getMarkedTag();
- if ( $lastrev !== false && $lastrev === $revision ) {
- $out->wrapWikiMsg(
- '<div class="warningbox">$1</div>',
- [ 'tpt-already-marked' ]
- );
- $this->listPages();
-
- return;
- }
+ $firstMark = $page->getMarkedTag() === false;
// This will modify the sections to include name property
$error = false;
@@ -226,25 +231,29 @@ class SpecialPageTranslation extends SpecialPage {
} );
}
- $err = $this->markForTranslation( $page, $sections );
+ $setVersion = $firstMark || $request->getCheck( 'use-latest-syntax' );
+
+ $err = $this->markForTranslation( $page, $sections, $setVersion );
if ( $err ) {
call_user_func_array( [ $out, 'addWikiMsg' ], $err );
} else {
- $this->showSuccess( $page );
+ $this->showSuccess( $page, $firstMark );
$this->listPages();
}
return;
}
- $this->showPage( $page, $sections );
+ $this->showPage( $page, $sections, $firstMark );
}
/**
+ * Displays success message and other instructions after a page has been marked for translation.
* @param TranslatablePage $page
+ * @param bool $firstMark true if it is the first time the page is being marked for translation.
*/
- public function showSuccess( TranslatablePage $page ) {
+ public function showSuccess( TranslatablePage $page, $firstMark = false ) {
$titleText = $page->getTitle()->getPrefixedText();
$num = $this->getLanguage()->formatNum( $page->getParse()->countSections() );
$link = SpecialPage::getTitleFor( 'Translate' )->getFullURL( [
@@ -258,9 +267,15 @@ class SpecialPageTranslation extends SpecialPage {
[ 'tpt-saveok', $titleText, $num, $link ]
);
+ // If the page is being marked for translation for the first time
+ // add a link to Special:PageMigration.
+ if ( $firstMark ) {
+ $this->getOutput()->addWikiMsg( 'tpt-saveok-first' );
+ }
+
// If TranslationNotifications is installed, and the user can notify
// translators, add a convenience link.
- if ( method_exists( 'SpecialNotifyTranslators', 'execute' ) &&
+ if ( method_exists( SpecialNotifyTranslators::class, 'execute' ) &&
$this->getUser()->isAllowed( SpecialNotifyTranslators::$right )
) {
$link = SpecialPage::getTitleFor( 'NotifyTranslators' )->getFullURL(
@@ -370,39 +385,52 @@ class SpecialPageTranslation extends SpecialPage {
}
/**
- * @param array $in
- * @return array
+ * Classify a list of pages and amend them with additional metadata.
+ *
+ * @param array[] $pages
+ * @return array[]
+ * @phan-return array{proposed:array[],active:array[],broken:array[],outdated:array[]}
*/
- protected function classifyPages( array $in ) {
+ private function classifyPages( array $pages ): array {
+ // Preload stuff for performance
+ $messageGroupIdsForPreload = [];
+ foreach ( $pages as $i => $page ) {
+ $id = TranslatablePage::getMessageGroupIdFromTitle( $page['title'] );
+ $messageGroupIdsForPreload[] = $id;
+ $pages[$i]['groupid'] = $id;
+ }
+ TranslateMetadata::preloadGroups( $messageGroupIdsForPreload );
+
$out = [
- 'proposed' => [],
+ // The ideal state for pages: marked and up to date
'active' => [],
+ 'proposed' => [],
+ 'outdated' => [],
'broken' => [],
- 'discouraged' => [],
];
- foreach ( $in as $index => $page ) {
+ foreach ( $pages as $page ) {
+ $group = MessageGroups::getGroup( $page['groupid'] );
+ $page['discouraged'] = MessageGroups::getPriority( $group ) === 'discouraged';
+ $page['version'] = TranslateMetadata::getWithDefaultValue(
+ $page['groupid'], 'version', self::DEFAULT_SYNTAX_VERSION
+ );
+
if ( !isset( $page['tp:mark'] ) ) {
// Never marked, check that the latest version is ready
if ( $page['tp:tag'] === $page['latest'] ) {
- $out['proposed'][$index] = $page;
+ $out['proposed'][] = $page;
} // Otherwise ignore such pages
} elseif ( $page['tp:tag'] === $page['latest'] ) {
- // Marked and latest version if fine
- $out['active'][$index] = $page;
+ if ( $page['tp:mark'] === $page['tp:tag'] ) {
+ // Marked and latest version is fine
+ $out['active'][] = $page;
+ } else {
+ $out['outdated'][] = $page;
+ }
} else {
- // Marked but latest version if not fine
- $out['broken'][$index] = $page;
- }
- }
-
- // broken and proposed take preference over discouraged status
- foreach ( $out['active'] as $index => $page ) {
- $id = TranslatablePage::getMessageGroupIdFromTitle( $page['title'] );
- $group = MessageGroups::getGroup( $id );
- if ( MessageGroups::getPriority( $group ) === 'discouraged' ) {
- $out['discouraged'][$index] = $page;
- unset( $out['active'][$index] );
+ // Marked but latest version is not fine
+ $out['broken'][] = $page;
}
}
@@ -433,73 +461,34 @@ class SpecialPageTranslation extends SpecialPage {
if ( count( $pages ) ) {
$out->wrapWikiMsg( '== $1 ==', 'tpt-new-pages-title' );
$out->addWikiMsg( 'tpt-new-pages', count( $pages ) );
- $out->addHTML( '<ol>' );
- foreach ( $pages as $page ) {
- $link = Linker::link( $page['title'] );
- $acts = $this->actionLinks( $page, 'proposed' );
- $out->addHTML( "<li>$link $acts</li>" );
- }
- $out->addHTML( '</ol>' );
- }
-
- $pages = $types['active'];
- if ( count( $pages ) ) {
- $out->wrapWikiMsg( '== $1 ==', 'tpt-old-pages-title' );
- $out->addWikiMsg( 'tpt-old-pages', count( $pages ) );
- $out->addHTML( '<ol>' );
- foreach ( $pages as $page ) {
- $link = Linker::link( $page['title'] );
- if ( $page['tp:mark'] !== $page['tp:tag'] ) {
- $link = "<strong>$link</strong>";
- }
-
- $acts = $this->actionLinks( $page, 'active' );
- $out->addHTML( "<li>$link $acts</li>" );
- }
- $out->addHTML( '</ol>' );
+ $out->addHTML( $this->getPageList( $pages, 'proposed' ) );
}
$pages = $types['broken'];
if ( count( $pages ) ) {
$out->wrapWikiMsg( '== $1 ==', 'tpt-other-pages-title' );
$out->addWikiMsg( 'tpt-other-pages', count( $pages ) );
- $out->addHTML( '<ol>' );
- foreach ( $pages as $page ) {
- $link = Linker::link( $page['title'] );
- $acts = $this->actionLinks( $page, 'broken' );
- $out->addHTML( "<li>$link $acts</li>" );
- }
- $out->addHTML( '</ol>' );
+ $out->addHTML( $this->getPageList( $pages, 'broken' ) );
}
- $pages = $types['discouraged'];
+ $pages = $types['outdated'];
if ( count( $pages ) ) {
- $out->wrapWikiMsg( '== $1 ==', 'tpt-discouraged-pages-title' );
- $out->addWikiMsg( 'tpt-discouraged-pages', count( $pages ) );
- $out->addHTML( '<ol>' );
- foreach ( $pages as $page ) {
- $link = Linker::link( $page['title'] );
- if ( $page['tp:mark'] !== $page['tp:tag'] ) {
- $link = "<strong>$link</strong>";
- }
+ $out->wrapWikiMsg( '== $1 ==', 'tpt-outdated-pages-title' );
+ $out->addWikiMsg( 'tpt-outdated-pages', count( $pages ) );
+ $out->addHTML( $this->getPageList( $pages, 'outdated' ) );
+ }
- $acts = $this->actionLinks( $page, 'discouraged' );
- $out->addHTML( "<li>$link $acts</li>" );
- }
- $out->addHTML( '</ol>' );
+ $pages = $types['active'];
+ if ( count( $pages ) ) {
+ $out->wrapWikiMsg( '== $1 ==', 'tpt-old-pages-title' );
+ $out->addWikiMsg( 'tpt-old-pages', count( $pages ) );
+ $out->addHTML( $this->getPageList( $pages, 'active' ) );
}
}
- /**
- * @param array $page
- * @param string $type
- * @return string
- */
- protected function actionLinks( array $page, $type ) {
+ private function actionLinks( array $page, string $type ): string {
$actions = [];
- /**
- * @var Title $title
- */
+ /** @var Title $title */
$title = $page['title'];
$user = $this->getUser();
@@ -507,8 +496,9 @@ class SpecialPageTranslation extends SpecialPage {
$js = [ 'class' => 'mw-translate-jspost' ];
if ( $user->isAllowed( 'pagetranslation' ) ) {
- $pending = $type === 'active' && $page['latest'] !== $page['tp:mark'];
- if ( $type === 'proposed' || $pending ) {
+ // Enable re-marking of all pages to allow changing of priority languages
+ // or migration to the new syntax version
+ if ( $type !== 'broken' ) {
$actions[] = Linker::linkKnown(
$this->getPageTitle(),
$this->msg( 'tpt-rev-mark' )->escaped(),
@@ -521,31 +511,31 @@ class SpecialPageTranslation extends SpecialPage {
);
}
- if ( $type === 'active' ) {
- $actions[] = Linker::linkKnown(
- $this->getPageTitle(),
- $this->msg( 'tpt-rev-discourage' )->escaped(),
- [ 'title' => $this->msg( 'tpt-rev-discourage-tooltip' )->text() ] + $js,
- [
- 'do' => 'discourage',
- 'target' => $title->getPrefixedText(),
- 'revision' => -1,
- ]
- );
- } elseif ( $type === 'discouraged' ) {
- $actions[] = Linker::linkKnown(
- $this->getPageTitle(),
- $this->msg( 'tpt-rev-encourage' )->escaped(),
- [ 'title' => $this->msg( 'tpt-rev-encourage-tooltip' )->text() ] + $js,
- [
- 'do' => 'encourage',
- 'target' => $title->getPrefixedText(),
- 'revision' => -1,
- ]
- );
- }
-
if ( $type !== 'proposed' ) {
+ if ( $page['discouraged'] ) {
+ $actions[] = Linker::linkKnown(
+ $this->getPageTitle(),
+ $this->msg( 'tpt-rev-encourage' )->escaped(),
+ [ 'title' => $this->msg( 'tpt-rev-encourage-tooltip' )->text() ] + $js,
+ [
+ 'do' => 'encourage',
+ 'target' => $title->getPrefixedText(),
+ 'revision' => -1,
+ ]
+ );
+ } else {
+ $actions[] = Linker::linkKnown(
+ $this->getPageTitle(),
+ $this->msg( 'tpt-rev-discourage' )->escaped(),
+ [ 'title' => $this->msg( 'tpt-rev-discourage-tooltip' )->text() ] + $js,
+ [
+ 'do' => 'discourage',
+ 'target' => $title->getPrefixedText(),
+ 'revision' => -1,
+ ]
+ );
+ }
+
$actions[] = Linker::linkKnown(
$this->getPageTitle(),
$this->msg( 'tpt-rev-unmark' )->escaped(),
@@ -563,13 +553,7 @@ class SpecialPageTranslation extends SpecialPage {
return '';
}
- $flattened = $this->getLanguage()->semicolonList( $actions );
-
- return Html::rawElement(
- 'span',
- [ 'class' => 'mw-tpt-actions' ],
- $this->msg( 'parentheses' )->rawParams( $flattened )->escaped()
- );
+ return '<div>' . $this->getLanguage()->pipeList( $actions ) . '</div>';
}
/**
@@ -584,30 +568,38 @@ class SpecialPageTranslation extends SpecialPage {
$sections = $parse->getSectionsForSave( $highest );
foreach ( $sections as $s ) {
+ if ( preg_match( '~[_/]~', $s->id ) ) {
+ $this->getOutput()->addElement(
+ 'p',
+ [ 'class' => 'errorbox' ],
+ $this->msg( 'tpt-invalid' )->params( $s->id )->text()
+ );
+ $error = true;
+ }
+
// We need to do checks for both new and existing sections.
// Someone might have tampered with the page source adding
// duplicate or invalid markers.
- if ( isset( $usedNames[$s->id] ) ) {
- $this->getOutput()->addWikiMsg( 'tpt-duplicate', $s->id );
+ $usedNames[$s->id] = ( $usedNames[$s->id] ?? 0 ) + 1;
+ $s->name = $s->id;
+ }
+ foreach ( $usedNames as $name => $count ) {
+ if ( $count > 1 ) {
+ // Only show error once per duplicated translation unit
+ $this->getOutput()->addElement(
+ 'p',
+ [ 'class' => 'errorbox' ],
+ $this->msg( 'tpt-duplicate' )->params( $name )->text()
+ );
$error = true;
}
- $usedNames[$s->id] = true;
- $s->name = $s->id;
}
-
return $sections;
}
- /**
- * Displays the sections and changes for the user to review
- * @param TranslatablePage $page
- * @param TPSection[] $sections
- */
- public function showPage( TranslatablePage $page, array $sections ) {
+ private function showPage( TranslatablePage $page, array $sections, bool $firstMark ): void {
$out = $this->getOutput();
-
$out->setSubtitle( Linker::link( $page->getTitle() ) );
-
$out->addWikiMsg( 'tpt-showpage-intro' );
$formParams = [
@@ -643,12 +635,18 @@ class SpecialPageTranslation extends SpecialPage {
$s->type = $defaultChecked ? $s->type : 'new';
// Checkbox for page title optional translation
- $this->getOutput()->addHTML( Xml::checkLabel(
- $this->msg( 'tpt-translate-title' )->text(),
- 'translatetitle',
- 'mw-translate-title',
- $defaultChecked
- ) );
+ $checkBox = new OOUI\FieldLayout(
+ new OOUI\CheckboxInputWidget( [
+ 'name' => 'translatetitle',
+ 'selected' => $defaultChecked,
+ ] ),
+ [
+ 'label' => $this->msg( 'tpt-translate-title' )->text(),
+ 'align' => 'inline',
+ 'classes' => [ 'mw-tpt-m-vertical' ]
+ ]
+ );
+ $out->addHTML( $checkBox->toString() );
}
if ( $s->type === 'new' ) {
@@ -674,13 +672,18 @@ class SpecialPageTranslation extends SpecialPage {
$diff->showDiffStyle();
$id = "tpt-sect-{$s->id}-action-nofuzzy";
- $checkLabel = Xml::checkLabel(
- $this->msg( 'tpt-action-nofuzzy' )->text(),
- $id,
- $id,
- false
+ $checkLabel = new OOUI\FieldLayout(
+ new OOUI\CheckboxInputWidget( [
+ 'name' => $id,
+ 'selected' => false,
+ ] ),
+ [
+ 'label' => $this->msg( 'tpt-action-nofuzzy' )->text(),
+ 'align' => 'inline',
+ 'classes' => [ 'mw-tpt-m-vertical' ]
+ ]
);
- $text = $checkLabel . $text;
+ $text = $checkLabel->toString() . $text;
} else {
$text = TranslateUtils::convertWhiteSpaceToHTML( $s->getText() );
}
@@ -700,9 +703,7 @@ class SpecialPageTranslation extends SpecialPage {
$hasChanges = true;
$out->wrapWikiMsg( '==$1==', 'tpt-sections-deleted' );
- /**
- * @var TPSection $s
- */
+ /** @var TPSection $s */
foreach ( $deletedSections as $s ) {
$name = $this->msg( 'tpt-section-deleted', $s->id )->escaped();
$text = TranslateUtils::convertWhiteSpaceToHTML( $s->getText() );
@@ -752,65 +753,98 @@ class SpecialPageTranslation extends SpecialPage {
$out->wrapWikiMsg( '<div class="successbox">$1</div>', 'tpt-mark-nochanges' );
}
+ $version = TranslateMetadata::getWithDefaultValue(
+ $page->getMessageGroupId(), 'version', self::DEFAULT_SYNTAX_VERSION
+ );
$this->priorityLanguagesForm( $page );
- $out->addHTML(
- Xml::submitButton( $this->msg( 'tpt-submit' )->text() ) .
- Xml::closeElement( 'form' )
+ $this->syntaxVersionForm( $version, $firstMark );
+
+ $submitButton = new OOUI\FieldLayout(
+ new OOUI\ButtonInputWidget( [
+ 'label' => $this->msg( 'tpt-submit' )->text(),
+ 'type' => 'submit',
+ 'flags' => [ 'primary', 'progressive' ],
+ ] ),
+ [
+ 'label' => null,
+ 'align' => 'top',
+ ]
);
- }
- /**
- * @param TranslatablePage $page
- */
- protected function priorityLanguagesForm( TranslatablePage $page ) {
- global $wgContLang;
+ $out->addHTML( $submitButton->toString() );
+ $out->addHTML( '</form>' );
+ }
+ private function priorityLanguagesForm( TranslatablePage $page ): void {
$groupId = $page->getMessageGroupId();
+
+ $form = new OOUI\FieldsetLayout( [
+ 'items' => [
+ new OOUI\FieldLayout(
+ new OOUI\TextInputWidget( [
+ 'name' => 'prioritylangs',
+ 'value' => TranslateMetadata::get( $groupId, 'prioritylangs' ),
+ 'inputId' => 'tpt-prioritylangs',
+ 'dir' => 'ltr',
+ ] ),
+ [
+ 'label' => $this->msg( 'tpt-select-prioritylangs' )->text(),
+ 'align' => 'top',
+ ]
+ ),
+ new OOUI\FieldLayout(
+ new OOUI\CheckboxInputWidget( [
+ 'name' => 'forcelimit',
+ 'selected' => TranslateMetadata::get( $groupId, 'priorityforce' ) === 'on',
+ ] ),
+ [
+ 'label' => $this->msg( 'tpt-select-prioritylangs-force' )->text(),
+ 'align' => 'inline',
+ ]
+ ),
+ new OOUI\FieldLayout(
+ new OOUI\TextInputWidget( [
+ 'name' => 'priorityreason',
+ ] ),
+ [
+ 'label' => $this->msg( 'tpt-select-prioritylangs-reason' )->text(),
+ 'align' => 'top',
+ ]
+ ),
+
+ ]
+ ] );
+
$this->getOutput()->wrapWikiMsg( '==$1==', 'tpt-sections-prioritylangs' );
+ $this->getOutput()->addHTML( $form->toString() );
+ }
- $langSelector = Xml::languageSelector(
- $wgContLang->getCode(),
- false,
- $this->getLanguage()->getCode()
- );
+ private function syntaxVersionForm( string $version, bool $firstMark ): void {
+ $out = $this->getOutput();
- $hLangs = Xml::inputLabelSep(
- $this->msg( 'tpt-select-prioritylangs' )->text(),
- 'prioritylangs', // name
- 'tpt-prioritylangs', // id
- 50,
- TranslateMetadata::get( $groupId, 'prioritylangs' )
- );
+ if ( $version === self::LATEST_SYNTAX_VERSION || $firstMark ) {
+ return;
+ }
- $hForce = Xml::checkLabel(
- $this->msg( 'tpt-select-prioritylangs-force' )->text(),
- 'forcelimit', // name
- 'tpt-priority-forcelimit', // id
- TranslateMetadata::get( $groupId, 'priorityforce' ) === 'on'
+ $out->wrapWikiMsg( '==$1==', 'tpt-sections-syntaxversion' );
+ $out->addWikiMsg(
+ 'tpt-syntaxversion-text',
+ '<code>' . wfEscapeWikiText( '<span lang="en" dir="ltr">...</span>' ) . '</code>',
+ '<code>' . wfEscapeWikiText( '<translate nowrap>...</translate>' ) . '</code>'
);
- $hReason = Xml::inputLabelSep(
- $this->msg( 'tpt-select-prioritylangs-reason' )->text(),
- 'priorityreason', // name
- 'tpt-priority-reason', // id
- 50, // size
- TranslateMetadata::get( $groupId, 'priorityreason' )
+ $checkBox = new OOUI\FieldLayout(
+ new OOUI\CheckboxInputWidget( [
+ 'name' => 'use-latest-syntax'
+ ] ),
+ [
+ 'label' => $out->msg( 'tpt-syntaxversion-label' )->text(),
+ 'align' => 'inline',
+ ]
);
- $this->getOutput()->addHTML(
- '<table>' .
- '<tr>' .
- "<td class='mw-label'>$hLangs[0]</td>" .
- "<td class='mw-input'>$hLangs[1]$langSelector[1]</td>" .
- '</tr>' .
- "<tr><td></td><td class='mw-inout'>$hForce</td></tr>" .
- '<tr>' .
- "<td class='mw-label'>$hReason[0]</td>" .
- "<td class='mw-input'>$hReason[1]</td>" .
- '</tr>' .
- '</table>'
- );
+ $out->addHTML( $checkBox->toString() );
}
/**
@@ -822,9 +856,14 @@ class SpecialPageTranslation extends SpecialPage {
* - Invalidates caches
* @param TranslatablePage $page
* @param TPSection[] $sections
+ * @param bool $updateVersion
* @return array|bool
*/
- public function markForTranslation( TranslatablePage $page, array $sections ) {
+ protected function markForTranslation(
+ TranslatablePage $page,
+ array $sections,
+ bool $updateVersion
+ ) {
// Add the section markers to the source page
$wikiPage = WikiPage::factory( $page->getTitle() );
$content = ContentHandler::makeContent(
@@ -842,23 +881,42 @@ class SpecialPageTranslation extends SpecialPage {
return [ 'tpt-edit-failed', $status->getWikiText() ];
}
- $newrevision = $status->value['revision'];
+ if ( version_compare( MW_VERSION, '1.35', '>=' ) ) {
+ // MW 1.35+
+ // Cannot use array_key_exists with DeprecatablePropertyArray, and
+ // $status->value['revision-record'] can be null, so isset doesn't
+ // work either
+ // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
+ $newRevisionRecord = $status->value['revision-record'];
+ } else {
+ // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
+ $newRevision = $status->value['revision'];
+ if ( $newRevision instanceof Revision ) {
+ $newRevisionRecord = $newRevision->getRevisionRecord();
+ } else {
+ $newRevisionRecord = null;
+ }
+ }
- // In theory it is either null or Revision object,
- // never revision object with null id, but who knows
- if ( $newrevision instanceof Revision ) {
- $newrevision = $newrevision->getId();
+ // In theory it is either null or RevisionRecord object,
+ // not a RevisionRecord object with null id, but who knows
+ if ( $newRevisionRecord instanceof RevisionRecord ) {
+ $newRevisionId = $newRevisionRecord->getId();
+ } else {
+ $newRevisionId = null;
}
- if ( $newrevision === null ) {
- // Probably a no-change edit, so no new revision was assigned.
- // Get the latest revision manually
- $newrevision = $page->getTitle()->getLatestRevID();
+ // Probably a no-change edit, so no new revision was assigned.
+ // Get the latest revision manually
+ // Could also occur on the off chance $newRevisionRecord->getId() returns null
+ if ( $newRevisionId === null ) {
+ $newRevisionId = $page->getTitle()->getLatestRevID();
}
$inserts = [];
$changed = [];
- $maxid = (int)TranslateMetadata::get( $page->getMessageGroupId(), 'maxid' );
+ $groupId = $page->getMessageGroupId();
+ $maxid = (int)TranslateMetadata::get( $groupId, 'maxid' );
$pageId = $page->getTitle()->getArticleID();
/**
@@ -888,11 +946,17 @@ class SpecialPageTranslation extends SpecialPage {
__METHOD__
);
$dbw->insert( 'translate_sections', $inserts, __METHOD__ );
- TranslateMetadata::set( $page->getMessageGroupId(), 'maxid', $maxid );
+ TranslateMetadata::set( $groupId, 'maxid', $maxid );
+ if ( $updateVersion ) {
+ TranslateMetadata::set( $groupId, 'version', self::LATEST_SYNTAX_VERSION );
+ }
- $page->addMarkedTag( $newrevision );
+ $page->addMarkedTag( $newRevisionId );
MessageGroups::singleton()->recache();
+ $group = $page->getMessageGroup();
+ $newKeys = $group->makeGroupKeys( $changed );
+ MessageIndex::singleton()->storeInterim( $group, $newKeys );
$job = TranslationsUpdateJob::newFromPage( $page, $sections );
JobQueueGroup::singleton()->push( $job );
@@ -903,7 +967,7 @@ class SpecialPageTranslation extends SpecialPage {
$entry->setPerformer( $this->getUser() );
$entry->setTarget( $page->getTitle() );
$entry->setParameters( [
- 'revision' => $newrevision,
+ 'revision' => $newRevisionId,
'changed' => count( $changed ),
] );
$logid = $entry->insert();
@@ -977,8 +1041,39 @@ class SpecialPageTranslation extends SpecialPage {
* @since 2014.09
*/
public static function getStrippedSourcePageText( TPParse $parse ) {
- $text = $parse->getTranslationPageText( [] );
+ $text = $parse->getTranslationPageText( null );
$text = preg_replace( '~<languages\s*/>\n?~s', '', $text );
return $text;
}
+
+ private function getPageList( array $pages, string $type ): string {
+ $items = [];
+
+ foreach ( $pages as $page ) {
+ $link = Linker::link( $page['title'] );
+ $acts = $this->actionLinks( $page, $type );
+ $tags = [];
+ if ( $page['discouraged'] ) {
+ $tags[] = $this->msg( 'tpt-tag-discouraged' )->escaped();
+ }
+ if ( $type !== 'proposed' && $page['version'] !== self::LATEST_SYNTAX_VERSION ) {
+ $tags[] = $this->msg( 'tpt-tag-oldsyntax' )->escaped();
+ }
+
+ $tagList = '';
+ if ( $tags ) {
+ $tagList = Html::rawElement(
+ 'span',
+ [ 'class' => 'mw-tpt-actions' ],
+ $this->msg( 'parentheses' )->rawParams(
+ $this->getLanguage()->pipeList( $tags )
+ )->escaped()
+ );
+ }
+
+ $items[] = "<li>$link $tagList $acts</li>";
+ }
+
+ return '<ol>' . implode( "", $items ) . '</ol>';
+ }
}
diff --git a/MLEB/Translate/tag/SpecialPageTranslationDeletePage.php b/MLEB/Translate/tag/SpecialPageTranslationDeletePage.php
index 8fe011dc..9ef78028 100644
--- a/MLEB/Translate/tag/SpecialPageTranslationDeletePage.php
+++ b/MLEB/Translate/tag/SpecialPageTranslationDeletePage.php
@@ -7,6 +7,8 @@
* @license GPL-2.0-or-later
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Special page which enables deleting translations of translatable pages
*
@@ -39,8 +41,9 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
/// Contains the language code if we are working with translation page
protected $code;
- protected $sectionPages;
-
+ /**
+ * @var Title[]
+ */
protected $translationPages;
public function __construct() {
@@ -56,6 +59,8 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
}
public function execute( $par ) {
+ $this->addhelpLink( 'Help:Deletion_and_undeletion' );
+
$request = $this->getRequest();
$par = (string)$par;
@@ -147,15 +152,14 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
throw new ErrorPageError( 'nopagetitle', 'nopagetext' );
}
- $permissionErrors = $this->title->getUserPermissionsErrors( 'delete', $this->getUser() );
+ $permissionErrors = MediaWikiServices::getInstance()->getPermissionManager()
+ ->getPermissionErrors( 'delete', $this->getUser(), $this->title );
if ( count( $permissionErrors ) ) {
throw new PermissionsError( 'delete', $permissionErrors );
}
# Check for database lock
- if ( wfReadOnly() ) {
- throw new ReadOnlyError;
- }
+ $this->checkReadOnly();
// Let the caller know it's safe to continue
return true;
@@ -176,22 +180,7 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
protected function showForm() {
$this->getOutput()->addWikiMsg( 'pt-deletepage-intro' );
- $formDescriptor = [
- 'wpTitle' => [
- 'type' => 'text',
- 'name' => 'wpTitle',
- 'label' => $this->msg( 'pt-deletepage-current' )->text(),
- 'size' => 30,
- 'default' => $this->text,
- ],
- 'wpReason' => [
- 'type' => 'text',
- 'name' => 'wpReason',
- 'label' => $this->msg( 'pt-deletepage-reason' )->text(),
- 'size' => 60,
- 'default' => $this->reason,
- ]
- ];
+ $formDescriptor = $this->getCommonFormFields();
$htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
$htmlForm
@@ -212,14 +201,14 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
protected function showConfirmation() {
$out = $this->getOutput();
$count = 0;
+ $subpageCount = 0;
$out->addWikiMsg( 'pt-deletepage-intro' );
$out->wrapWikiMsg( '== $1 ==', 'pt-deletepage-list-pages' );
if ( !$this->singleLanguage() ) {
$count++;
- TranslateUtils::addWikiTextAsInterface(
- $out,
+ $out->addWikiTextAsInterface(
$this->getChangeLine( $this->title )
);
}
@@ -231,7 +220,7 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
$count++;
$lines[] = $this->getChangeLine( $old );
}
- TranslateUtils::addWikiTextAsInterface( $out, implode( "\n", $lines ) );
+ $this->listPages( $out, $lines );
$out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-section' );
$sectionPages = $this->getSectionPages();
@@ -240,50 +229,39 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
$count++;
$lines[] = $this->getChangeLine( $old );
}
- TranslateUtils::addWikiTextAsInterface( $out, implode( "\n", $lines ) );
+ $this->listPages( $out, $lines );
- $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-other' );
- $subpages = $this->getSubpages();
- $lines = [];
- foreach ( $subpages as $old ) {
- if ( TranslatablePage::isTranslationPage( $old ) ) {
- continue;
- }
+ if ( TranslateUtils::allowsSubpages( $this->title ) ) {
+ $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-other' );
+ $subpages = $this->getSubpages();
+ $lines = [];
+ foreach ( $subpages as $old ) {
+ if ( TranslatablePage::isTranslationPage( $old ) ) {
+ continue;
+ }
- if ( $this->doSubpages ) {
- $count++;
+ $subpageCount++;
+ $lines[] = $this->getChangeLine( $old );
}
-
- $lines[] = $this->getChangeLine( $old, $this->doSubpages );
+ $this->listPages( $out, $lines );
}
- TranslateUtils::addWikiTextAsInterface( $out, implode( "\n", $lines ) );
- TranslateUtils::addWikiTextAsInterface( $out, "----\n" );
- $out->addWikiMsg( 'pt-deletepage-list-count', $this->getLanguage()->formatNum( $count ) );
+ $totalPageCount = $count + $subpageCount;
- $formDescriptor = [
- 'wpTitle' => [
- 'type' => 'text',
- 'name' => 'wpTitle',
- 'label' => $this->msg( 'pt-deletepage-current' )->text(),
- 'size' => 30,
- 'default' => $this->text,
- 'readonly' => true,
- ],
- 'wpReason' => [
- 'type' => 'text',
- 'name' => 'wpReason',
- 'label' => $this->msg( 'pt-deletepage-reason' )->text(),
- 'size' => 60,
- 'default' => $this->reason,
- ],
- 'subpages' => [
- 'type' => 'check',
- 'name' => 'subpages',
- 'id' => 'mw-subpages',
- 'label' => $this->msg( 'pt-deletepage-subpages' )->text(),
- 'default' => $this->doSubpages,
- ]
+ $out->addWikiTextAsInterface( "----\n" );
+ $out->addWikiMsg(
+ 'pt-deletepage-list-count',
+ $this->getLanguage()->formatNum( $totalPageCount ),
+ $this->getLanguage()->formatNum( $subpageCount )
+ );
+
+ $formDescriptor = $this->getCommonFormFields();
+ $formDescriptor['subpages'] = [
+ 'type' => 'check',
+ 'name' => 'subpages',
+ 'id' => 'mw-subpages',
+ 'label' => $this->msg( 'pt-deletepage-subpages' )->text(),
+ 'default' => $this->doSubpages,
];
$htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
@@ -310,15 +288,10 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
/**
* @param Title $title
- * @param bool $enabled
* @return string One line of wikitext, without trailing newline.
*/
- protected function getChangeLine( $title, $enabled = true ) {
- if ( $enabled ) {
- return '* ' . $title->getPrefixedText();
- } else {
- return '* <s>' . $title->getPrefixedText() . '</s>';
- }
+ protected function getChangeLine( $title ) {
+ return '* ' . $title->getPrefixedText();
}
protected function performAction() {
@@ -366,6 +339,16 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
}
}
+ if ( !$this->singleLanguage() ) {
+ $jobs[$this->title->getPrefixedText()] = TranslateDeleteJob::newJob(
+ $this->title,
+ $base,
+ !$this->singleLanguage(),
+ $user,
+ $this->reason
+ );
+ }
+
JobQueueGroup::singleton()->push( $jobs );
$cache = wfGetCache( CACHE_DB );
@@ -393,22 +376,21 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
TranslateMetadata::set( $groupId, 'priorityforce', false );
TranslateMetadata::set( $groupId, 'priorityreason', false );
// remove the page from aggregate groups, if present in any of them.
- $groups = MessageGroups::getAllGroups();
- foreach ( $groups as $group ) {
- if ( $group instanceof AggregateMessageGroup ) {
- $subgroups = TranslateMetadata::get( $group->getId(), 'subgroups' );
- if ( $subgroups !== false ) {
- $subgroups = explode( ',', $subgroups );
+ $aggregateGroups = MessageGroups::getGroupsByType( AggregateMessageGroup::class );
+ TranslateMetadata::preloadGroups( array_keys( $aggregateGroups ) );
+ foreach ( $aggregateGroups as $group ) {
+ $subgroups = TranslateMetadata::get( $group->getId(), 'subgroups' );
+ if ( $subgroups !== false ) {
+ $subgroups = explode( ',', $subgroups );
+ $subgroups = array_flip( $subgroups );
+ if ( isset( $subgroups[$groupId] ) ) {
+ unset( $subgroups[$groupId] );
$subgroups = array_flip( $subgroups );
- if ( isset( $subgroups[$groupId] ) ) {
- unset( $subgroups[$groupId] );
- $subgroups = array_flip( $subgroups );
- TranslateMetadata::set(
- $group->getId(),
- 'subgroups',
- implode( ',', $subgroups )
- );
- }
+ TranslateMetadata::set(
+ $group->getId(),
+ 'subgroups',
+ implode( ',', $subgroups )
+ );
}
}
}
@@ -416,7 +398,7 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
/**
* Returns all section pages, including those which are currently not active.
- * @return Array of titles.
+ * @return Title[]
*/
protected function getSectionPages() {
$code = $this->singleLanguage() ? $this->code : false;
@@ -426,7 +408,7 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
/**
* Returns only translation subpages.
- * @return Array of titles.
+ * @return Title[]
*/
protected function getTranslationPages() {
if ( $this->singleLanguage() ) {
@@ -454,4 +436,32 @@ class SpecialPageTranslationDeletePage extends SpecialPage {
protected function singleLanguage() {
return $this->code !== '';
}
+
+ protected function getCommonFormFields() {
+ return [
+ 'wpTitle' => [
+ 'type' => 'text',
+ 'name' => 'wpTitle',
+ 'label' => $this->msg( 'pt-deletepage-current' )->text(),
+ 'size' => 30,
+ 'default' => $this->text,
+ 'readonly' => true,
+ ],
+ 'wpReason' => [
+ 'type' => 'text',
+ 'name' => 'wpReason',
+ 'label' => $this->msg( 'pt-deletepage-reason' )->text(),
+ 'size' => 60,
+ 'default' => $this->reason,
+ ]
+ ];
+ }
+
+ protected function listPages( OutputPage $out, array $lines ): void {
+ if ( $lines ) {
+ $out->addWikiTextAsInterface( implode( "\n", $lines ) );
+ } else {
+ $out->addWikiMsg( 'pt-deletepage-list-no-pages' );
+ }
+ }
}
diff --git a/MLEB/Translate/tag/SpecialPageTranslationMovePage.php b/MLEB/Translate/tag/SpecialPageTranslationMovePage.php
index 452f65fb..87dd6fb2 100644
--- a/MLEB/Translate/tag/SpecialPageTranslationMovePage.php
+++ b/MLEB/Translate/tag/SpecialPageTranslationMovePage.php
@@ -7,6 +7,8 @@
* @license GPL-2.0-or-later
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Overrides Special:Movepage to to allow renaming a page translation page and
* all related translations and derivative pages.
@@ -17,11 +19,6 @@ class SpecialPageTranslationMovePage extends MovePageForm {
// Basic form parameters both as text and as titles
protected $newText, $oldText;
- /**
- * @var Title
- */
- protected $newTitle, $oldTitle;
-
// Other form parameters
/**
* 'check' or 'perform'
@@ -29,16 +26,6 @@ class SpecialPageTranslationMovePage extends MovePageForm {
protected $subaction;
/**
- * There must be reason for everything.
- */
- protected $reason;
-
- /**
- * Allow skipping non-translation subpages.
- */
- protected $moveSubpages;
-
- /**
* @var TranslatablePage instance.
*/
protected $page;
@@ -58,18 +45,6 @@ class SpecialPageTranslationMovePage extends MovePageForm {
*/
protected $sectionPages;
- public function __construct() {
- parent::__construct( 'Movepage' );
- }
-
- public function doesWrites() {
- return true;
- }
-
- public function isListed() {
- return false;
- }
-
/**
* Partially copies from SpecialMovepage.php, because it cannot be
* extended in other ways.
@@ -80,14 +55,13 @@ class SpecialPageTranslationMovePage extends MovePageForm {
public function execute( $par ) {
$request = $this->getRequest();
$user = $this->getUser();
-
- $par = is_null( $par ) ? '' : $par; // Title::newFromText expects strings only
+ $this->addHelpLink( 'Help:Extension:Translate/Move_translatable_page' );
// Yes, the use of getVal() and getText() is wanted, see bug T22365
$this->oldText = $request->getVal( 'wpOldTitle', $request->getVal( 'target', $par ) );
$this->newText = $request->getText( 'wpNewTitle' );
- $this->oldTitle = Title::newFromText( $this->oldText );
+ $this->oldTitle = Title::newFromText( $this->oldText ?? '' );
$this->newTitle = Title::newFromText( $this->newText );
$this->reason = $request->getText( 'reason' );
@@ -125,7 +99,7 @@ class SpecialPageTranslationMovePage extends MovePageForm {
}
if ( $subaction === 'check' && $this->checkToken() && $request->wasPosted() ) {
- $blockers = $this->checkMoveBlockers();
+ $blockers = $this->checkMoveBlockers( $user );
if ( count( $blockers ) ) {
$this->showErrors( $blockers );
$this->showForm( [] );
@@ -165,7 +139,8 @@ class SpecialPageTranslationMovePage extends MovePageForm {
}
// Check rights
- $permErrors = $this->oldTitle->getUserPermissionsErrors( 'move', $this->getUser() );
+ $permErrors = MediaWikiServices::getInstance()->getPermissionManager()
+ ->getPermissionErrors( 'move', $this->getUser(), $this->oldTitle );
if ( count( $permErrors ) ) {
throw new PermissionsError( 'move', $permErrors );
}
@@ -183,24 +158,28 @@ class SpecialPageTranslationMovePage extends MovePageForm {
/**
* Pretty-print the list of errors.
- * @param array $errors Array with message key and parameters
+ * @param SplObjectStorage $errors Array with message key and parameters
*/
- protected function showErrors( array $errors ) {
- if ( count( $errors ) ) {
- $out = $this->getOutput();
-
- $out->addHTML( Html::openElement( 'div', [ 'class' => 'error' ] ) );
- $out->addWikiMsg(
- 'pt-movepage-blockers',
- $this->getLanguage()->formatNum( count( $errors ) )
- );
- $s = '';
- foreach ( $errors as $error ) {
- $s .= '* ' . wfMessage( ...$error )->plain() . "\n";
- }
- TranslateUtils::addWikiTextAsInterface( $out, $s );
- $out->addHTML( '</div>' );
+ protected function showErrors( SplObjectStorage $errors ) {
+ $out = $this->getOutput();
+
+ $out->addHtml( Html::openElement( 'div', [ 'class' => 'errorbox' ] ) );
+ $out->addWikiMsg(
+ 'pt-movepage-blockers',
+ $this->getLanguage()->formatNum( count( $errors ) )
+ );
+
+ // If there are many errors, for performance reasons we must parse them all at once
+ $s = '';
+ $context = 'pt-movepage-error-placeholder';
+ foreach ( $errors as $title ) {
+ // @phan-suppress-next-line PhanTypeSuspiciousStringExpression
+ $s .= "'''$title'''\n\n";
+ $s .= $errors[ $title ]->getWikiText( false, $context );
}
+
+ $out->addWikiTextAsInterface( $s );
+ $out->addHtml( Html::closeElement( 'div' ) );
}
/**
@@ -234,13 +213,6 @@ class SpecialPageTranslationMovePage extends MovePageForm {
'label' => $this->msg( 'pt-movepage-reason' )->text(),
'size' => 45,
'default' => $this->reason,
- ],
- 'subpages' => [
- 'type' => 'check',
- 'name' => 'subpages',
- 'id' => 'mw-subpages',
- 'label' => $this->msg( 'pt-movepage-subpages' )->text(),
- 'default' => $this->moveSubpages,
]
];
@@ -295,46 +267,57 @@ class SpecialPageTranslationMovePage extends MovePageForm {
$base = $this->oldTitle->getPrefixedText();
$target = $this->newTitle;
$count = 0;
+ $subpagesCount = 0;
$types = [
'pt-movepage-list-pages' => [ $this->oldTitle ],
'pt-movepage-list-translation' => $this->getTranslationPages(),
'pt-movepage-list-section' => $this->getSectionPages(),
- 'pt-movepage-list-translatable' => $this->getTranslatableSubpages(),
- 'pt-movepage-list-other' => $this->getNormalSubpages(),
+ 'pt-movepage-list-translatable' => $this->getTranslatableSubpages()
];
+ if ( TranslateUtils::allowsSubpages( $this->oldTitle ) ) {
+ $types[ 'pt-movepage-list-other'] = $this->getNormalSubpages();
+ }
+
foreach ( $types as $type => $pages ) {
- $out->wrapWikiMsg( '=== $1 ===', [ $type, count( $pages ) ] );
+ $pageCount = count( $pages );
+ $out->wrapWikiMsg( '=== $1 ===', [ $type, $pageCount ] );
+
+ if ( !$pageCount ) {
+ $out->addWikiMsg( 'pt-movepage-list-no-pages' );
+ continue;
+ }
+
if ( $type === 'pt-movepage-list-translatable' ) {
- $out->addWikiMsg( 'pt-movepage-list-translatable-note' );
+ $out->wrapWikiMsg(
+ "'''$1'''", $this->msg( 'pt-movepage-list-translatable-note' )
+ );
}
$lines = [];
foreach ( $pages as $old ) {
- $toBeMoved = true;
-
- // These pages need specific checks
- if ( $type === 'pt-movepage-list-other' ) {
- $toBeMoved = $this->moveSubpages;
- }
-
- if ( $type === 'pt-movepage-list-translatable' ) {
- $toBeMoved = false;
+ $canBeMoved = $type !== 'pt-movepage-list-translatable';
+ if ( $canBeMoved ) {
+ $count++;
}
- if ( $toBeMoved ) {
- $count++;
+ if ( $type === 'pt-movepage-list-other' ) {
+ $subpagesCount++;
}
- $lines[] = $this->getChangeLine( $base, $old, $target, $toBeMoved );
+ $lines[] = $this->getChangeLine( $base, $old, $target, $canBeMoved );
}
- TranslateUtils::addWikiTextAsInterface( $out, implode( "\n", $lines ) );
+ $out->addWikiTextAsInterface( implode( "\n", $lines ) );
}
- TranslateUtils::addWikiTextAsInterface( $out, "----\n" );
- $out->addWikiMsg( 'pt-movepage-list-count', $this->getLanguage()->formatNum( $count ) );
+ $out->addWikiTextAsInterface( "----\n" );
+ $out->addWikiMsg(
+ 'pt-movepage-list-count',
+ $this->getLanguage()->formatNum( $count ),
+ $this->getLanguage()->formatNum( $subpagesCount )
+ );
$br = Html::element( 'br' );
$readonly = [ 'readonly' => 'readonly' ];
@@ -438,57 +421,62 @@ class SpecialPageTranslationMovePage extends MovePageForm {
$this->getOutput()->addWikiMsg( 'pt-movepage-started' );
}
- protected function checkMoveBlockers() {
- $blockers = [];
+ protected function checkMoveBlockers( User $user ) {
+ $blockers = new SplObjectStorage();
+ $source = $this->oldTitle;
$target = $this->newTitle;
if ( !$target ) {
- $blockers[] = [ 'pt-movepage-block-base-invalid' ];
+ $blockers[$source] = Status::newFatal( 'pt-movepage-block-base-invalid' );
return $blockers;
}
if ( $target->inNamespaces( NS_MEDIAWIKI, NS_TRANSLATIONS ) ) {
- $blockers[] = [ 'immobile-target-namespace', $target->getNsText() ];
+ $blockers[$source] = Status::newFatal(
+ 'immobile-target-namespace', $target->getNsText()
+ );
return $blockers;
}
- $base = $this->oldTitle->getPrefixedText();
-
if ( $target->exists() ) {
- $blockers[] = [ 'pt-movepage-block-base-exists', $target->getPrefixedText() ];
+ $blockers[$source] = Status::newFatal(
+ 'pt-movepage-block-base-exists', $target->getPrefixedText()
+ );
} else {
- $errors = $this->oldTitle->isValidMoveOperation( $target, true, $this->reason );
- if ( is_array( $errors ) ) {
- $blockers = array_merge( $blockers, $errors );
+ $movePage = new MovePage( $this->oldTitle, $target );
+ $status = $movePage->isValidMove();
+ $status->merge( $movePage->checkPermissions( $user, $this->reason ) );
+ if ( !$status->isOK() ) {
+ $blockers[$source] = $status;
}
}
// Don't spam the same errors for all pages if base page fails
- if ( $blockers ) {
+ if ( count( $blockers ) ) {
return $blockers;
}
// Collect all the old and new titles for checcks
$titles = [];
-
+ $base = $this->oldTitle->getPrefixedText();
$pages = $this->getTranslationPages();
foreach ( $pages as $old ) {
$titles['tp'][] = [ $old, $this->newPageTitle( $base, $old, $target ) ];
}
- $pages = $this->getSectionPages();
- foreach ( $pages as $old ) {
- $titles['section'][] = [ $old, $this->newPageTitle( $base, $old, $target ) ];
- }
-
$subpages = $this->moveSubpages ? $this->getNormalSubpages() : [];
foreach ( $subpages as $old ) {
$titles['subpage'][] = [ $old, $this->newPageTitle( $base, $old, $target ) ];
}
+ $pages = $this->getSectionPages();
+ foreach ( $pages as $old ) {
+ $titles['section'][] = [ $old, $this->newPageTitle( $base, $old, $target ) ];
+ }
+
// Check that all new titles are valid
$lb = new LinkBatch();
foreach ( $titles as $type => $list ) {
@@ -498,10 +486,10 @@ class SpecialPageTranslationMovePage extends MovePageForm {
foreach ( $list as $pair ) {
list( $old, $new ) = $pair;
if ( $new === null ) {
- $blockers[] = [
+ $blockers[$old] = Status::newFatal(
"pt-movepage-block-$type-invalid",
$old->getPrefixedText()
- ];
+ );
continue;
}
$lb->addObj( $old );
@@ -509,7 +497,7 @@ class SpecialPageTranslationMovePage extends MovePageForm {
}
}
- if ( $blockers ) {
+ if ( count( $blockers ) ) {
return $blockers;
}
@@ -522,23 +510,27 @@ class SpecialPageTranslationMovePage extends MovePageForm {
foreach ( $list as $pair ) {
list( $old, $new ) = $pair;
if ( $new->exists() ) {
- $blockers[] = [
+ $blockers[$old] = Status::newFatal(
"pt-movepage-block-$type-exists",
$old->getPrefixedText(),
$new->getPrefixedText()
- ];
+ );
} else {
/* This method has terrible performance:
* - 2 queries by core
* - 3 queries by lqt
* - and no obvious way to preload the data! */
- $errors = $old->isValidMoveOperation( $target, false );
- if ( is_array( $errors ) ) {
- $blockers = array_merge( $blockers, $errors );
+ $movePage = new MovePage( $old, $target );
+ $status = $movePage->isValidMove();
+ // Do not check for permissions here, as these pages are not editable/movable
+ // in regular use
+ if ( !$status->isOK() ) {
+ $blockers[$old] = $status;
}
- /* Because of the above, check only one of the possibly thousands
- * of section pages and assume rest are fine. */
+ /* Because of the poor performance, check only one of the possibly thousands
+ * of section pages and assume rest are fine. This assumes section pages are
+ * listed last in the array. */
if ( $type === 'section' ) {
break;
}
@@ -587,7 +579,7 @@ class SpecialPageTranslationMovePage extends MovePageForm {
/**
* Returns only translation subpages.
- * @return Array of titles.
+ * @return Title[]
*/
protected function getTranslationPages() {
if ( !isset( $this->translationPages ) ) {
@@ -599,15 +591,20 @@ class SpecialPageTranslationMovePage extends MovePageForm {
/**
* Returns all subpages, if the namespace has them enabled.
- * @return mixed TitleArray, or empty array if this page's namespace doesn't allow subpages
+ * @return Title[]
*/
protected function getSubpages() {
- return $this->page->getTitle()->getSubpages();
+ $pages = $this->page->getTitle()->getSubpages();
+ if ( $pages instanceof Traversable ) {
+ $pages = iterator_to_array( $pages );
+ }
+
+ return $pages;
}
private function getNormalSubpages() {
return array_filter(
- iterator_to_array( $this->getSubpages() ),
+ $this->getSubpages(),
function ( $page ) {
return !(
TranslatablePage::isTranslationPage( $page ) ||
@@ -619,7 +616,7 @@ class SpecialPageTranslationMovePage extends MovePageForm {
private function getTranslatableSubpages() {
return array_filter(
- iterator_to_array( $this->getSubpages() ),
+ $this->getSubpages(),
function ( $page ) {
return TranslatablePage::isSourcePage( $page );
}
diff --git a/MLEB/Translate/tag/TPParse.php b/MLEB/Translate/tag/TPParse.php
index 9e1b32bc..07a4813e 100644
--- a/MLEB/Translate/tag/TPParse.php
+++ b/MLEB/Translate/tag/TPParse.php
@@ -168,7 +168,7 @@ class TPParse {
* translation tags removed and outdated translation marked with a class
* mw-translate-fuzzy.
*
- * @param MessageCollection $collection Collection that holds translated messages.
+ * @param MessageCollection|array $collection Collection that holds translated messages.
* @param bool $showOutdated Whether to show outdated sections, wrapped in a HTML class.
* @return string Whole page as wikitext.
*/
@@ -201,12 +201,15 @@ class TPParse {
// We do not ever want to show explicit fuzzy marks in the rendered pages
$sectiontext = str_replace( TRANSLATE_FUZZY, '', $sectiontext );
- if ( $s->isInline() ) {
- $sectiontext = "<span class=\"mw-translate-fuzzy\">$sectiontext</span>";
- } else {
- // We add new lines around the text to avoid disturbing any mark-up that
- // has special handling on line start, such as lists.
- $sectiontext = "<div class=\"mw-translate-fuzzy\">\n$sectiontext\n</div>";
+ if ( $s->canWrap() ) {
+ if ( $s->isInline() ) {
+ $sectiontext = "<span class=\"mw-translate-fuzzy\">$sectiontext</span>";
+ } else {
+ // We add new lines around the text to avoid disturbing any mark-up that
+ // has special handling on line start, such as lists.
+ $sectiontext =
+ "<div class=\"mw-translate-fuzzy\">\n$sectiontext\n</div>";
+ }
}
}
}
@@ -230,21 +233,21 @@ class TPParse {
$nph = [];
$text = TranslatablePage::armourNowiki( $nph, $text );
- // Remove translation markup from the template to produce final text
+ // Remove translation markup from the template to produce the final text.
$cb = [ __CLASS__, 'replaceTagCb' ];
- $text = preg_replace_callback( '~(<translate>)(.*)(</translate>)~sU', $cb, $text );
+ $text = preg_replace_callback( '~<translate(?: nowrap)?>(.*?)</translate>~s', $cb, $text );
$text = TranslatablePage::unArmourNowiki( $nph, $text );
return $text;
}
/**
- * Chops of trailing or preceeding whitespace intelligently to avoid
- * build up of unintented whitespace.
+ * Chops of trailing or preceding whitespace intelligently to avoid
+ * build up of unintended whitespace.
* @param string[] $matches
* @return string
*/
protected static function replaceTagCb( $matches ) {
- return preg_replace( '~^\n|\n\z~', '', $matches[2] );
+ return preg_replace( '~^\n|\n\z~', '', $matches[1] );
}
}
diff --git a/MLEB/Translate/tag/TPSection.php b/MLEB/Translate/tag/TPSection.php
index 49b3f104..4624ded9 100644
--- a/MLEB/Translate/tag/TPSection.php
+++ b/MLEB/Translate/tag/TPSection.php
@@ -19,9 +19,9 @@ class TPSection {
public $id;
/**
- * @var string New name of the section, that will be saved to database.
+ * @var string|null New name of the section, that will be saved to database.
*/
- public $name;
+ public $name = null;
/**
* @var string Section text.
@@ -34,9 +34,9 @@ class TPSection {
public $type;
/**
- * @var string Text of previous version of this section.
+ * @var string|null Text of previous version of this section.
*/
- public $oldText;
+ public $oldText = null;
/**
* @var bool Whether this section is inline section.
@@ -44,6 +44,9 @@ class TPSection {
*/
protected $inline = false;
+ /** @var bool Whether wrapping the section is allowed */
+ private $canWrap = true;
+
/**
* @var int Version number for the serialization.
*/
@@ -63,6 +66,22 @@ class TPSection {
}
/**
+ * @param bool $value
+ * @since 2020.07
+ */
+ public function setCanWrap( bool $value ): void {
+ $this->canWrap = $value;
+ }
+
+ /**
+ * @return bool
+ * @since 2020.07
+ */
+ public function canWrap(): bool {
+ return $this->canWrap;
+ }
+
+ /**
* Returns section text unmodified.
* @return string Wikitext.
*/
@@ -97,7 +116,7 @@ class TPSection {
* @return string Wikitext.
*/
public function getMarkedText() {
- $id = isset( $this->name ) ? $this->name : $this->id;
+ $id = $this->name ?? $this->id;
$header = "<!--T:{$id}-->";
$re = '~^(=+.*?=+\s*?$)~m';
$rep = "\\1 $header";
@@ -121,7 +140,7 @@ class TPSection {
* @return string Wikitext.
*/
public function getOldText() {
- return isset( $this->oldText ) ? $this->oldText : $this->text;
+ return $this->oldText ?? $this->text;
}
/**
diff --git a/MLEB/Translate/tag/TranslatablePage.php b/MLEB/Translate/tag/TranslatablePage.php
index 51a32db9..f6a838cb 100644
--- a/MLEB/Translate/tag/TranslatablePage.php
+++ b/MLEB/Translate/tag/TranslatablePage.php
@@ -7,6 +7,10 @@
* @license GPL-2.0-or-later
*/
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\SlotRecord;
use Wikimedia\Rdbms\Database;
/**
@@ -21,7 +25,7 @@ class TranslatablePage {
protected $title;
/**
- * Text contents of the page.
+ * @var ?string Text contents of the page.
*/
protected $text;
@@ -39,11 +43,6 @@ class TranslatablePage {
protected $source;
/**
- * Whether the page contents is already loaded.
- */
- protected $init = false;
-
- /**
* Name of the section which contains the translated page title.
*/
protected $displayTitle = 'Page display title';
@@ -91,8 +90,10 @@ class TranslatablePage {
* @throws MWException
* @return self
*/
- public static function newFromRevision( Title $title, $revision ) {
- $rev = Revision::newFromTitle( $title, $revision );
+ public static function newFromRevision( Title $title, int $revision ) {
+ $rev = MediaWikiServices::getInstance()
+ ->getRevisionLookup()
+ ->getRevisionByTitle( $title, $revision );
if ( $rev === null ) {
throw new MWException( 'Revision is null' );
}
@@ -106,7 +107,7 @@ class TranslatablePage {
/**
* Constructs a translatable page from title.
- * The text of last marked revision is loaded when neded.
+ * The text of last marked revision is loaded when needed.
*
* @param Title $title
* @return self
@@ -128,29 +129,38 @@ class TranslatablePage {
/**
* Returns the text for this translatable page.
- * @throws MWException
* @return string
*/
- public function getText() {
- if ( $this->init === false ) {
- switch ( $this->source ) {
- case 'text':
- break;
- /** @noinspection PhpMissingBreakStatementInspection */
- case 'title':
- $this->revision = $this->getMarkedTag();
- case 'revision':
- $rev = Revision::newFromTitle( $this->getTitle(), $this->revision );
- $this->text = ContentHandler::getContentText( $rev->getContent() );
- break;
+ public function getText(): string {
+ if ( $this->text !== null ) {
+ return $this->text;
+ }
+
+ $page = $this->getTitle()->getPrefixedDBkey();
+
+ if ( $this->source === 'title' ) {
+ $revision = $this->getMarkedTag();
+ if ( !is_int( $revision ) ) {
+ throw new LogicException(
+ "Trying to load a text for $page which is not marked for translation"
+ );
}
+ $this->revision = $revision;
}
- if ( !is_string( $this->text ) ) {
- throw new MWException( 'We have no text' );
+ $flags = TranslateUtils::shouldReadFromMaster()
+ ? RevisionLookup::READ_LATEST
+ : RevisionLookup::READ_NORMAL;
+ $rev = MediaWikiServices::getInstance()
+ ->getRevisionLookup()
+ ->getRevisionByTitle( $this->getTitle(), $this->revision, $flags );
+ $text = ContentHandler::getContentText( $rev->getContent( SlotRecord::MAIN ) );
+
+ if ( !is_string( $text ) ) {
+ throw new RuntimeException( "Failed to load text for $page" );
}
- $this->init = true;
+ $this->text = $text;
return $this->text;
}
@@ -159,21 +169,11 @@ class TranslatablePage {
* Revision is null if object was constructed using newFromText.
* @return null|int
*/
- public function getRevision() {
+ public function getRevision(): ?int {
return $this->revision;
}
/**
- * Manually set a revision number to use loading page text.
- * @param int $revision
- */
- public function setRevision( $revision ) {
- $this->revision = $revision;
- $this->source = 'revision';
- $this->init = false;
- }
-
- /**
* Returns the source language of this translatable page. In other words
* the language in which the page without language code is written.
* @return string
@@ -282,41 +282,38 @@ class TranslatablePage {
$tagPlaceHolders = [];
while ( true ) {
- $re = '~(<translate>)(.*?)(</translate>)~s';
+ $re = '~(<translate(?: nowrap)?>)(.*?)</translate>~s';
$matches = [];
- $ok = preg_match_all( $re, $text, $matches, PREG_OFFSET_CAPTURE );
+ $ok = preg_match( $re, $text, $matches, PREG_OFFSET_CAPTURE );
- if ( $ok === 0 ) {
- break; // No matches
+ if ( $ok === 0 || $ok === false ) {
+ break; // No match or failure
}
- // Do-placehold for the whole stuff
+ $contentWithTags = $matches[0][0];
+ $contentWithoutTags = $matches[2][0];
+ // These are offsets to the content inside the tags in $text
+ $offsetStart = $matches[0][1];
+ $offsetEnd = $offsetStart + strlen( $contentWithTags );
+
+ // Replace the whole match with a placeholder
$ph = TranslateUtils::getPlaceholder();
- $start = $matches[0][0][1];
- $len = strlen( $matches[0][0][0] );
- $end = $start + $len;
- $text = self::index_replace( $text, $ph, $start, $end );
-
- // Sectionise the contents
- // Strip the surrounding tags
- $contents = $matches[0][0][0]; // full match
- $start = $matches[2][0][1] - $matches[0][0][1]; // bytes before actual content
- $len = strlen( $matches[2][0][0] ); // len of the content
- $end = $start + $len;
-
- $sectiontext = substr( $contents, $start, $len );
-
- if ( strpos( $sectiontext, '<translate>' ) !== false ) {
- throw new TPException( [ 'pt-parse-nested', $sectiontext ] );
+ $text = substr( $text, 0, $offsetStart ) . $ph . substr( $text, $offsetEnd );
+
+ if ( preg_match( '~<translate( nowrap)?>~', $contentWithoutTags ) !== 0 ) {
+ throw new TPException( [ 'pt-parse-nested', $contentWithoutTags ] );
}
- $sectiontext = self::unArmourNowiki( $nowiki, $sectiontext );
+ $openTag = $matches[1][0];
+ $canWrap = $openTag !== '<translate nowrap>';
- $parse = self::sectionise( $sectiontext );
- $sections += $parse['sections'];
+ // Parse the content inside the tags
+ $contentWithoutTags = self::unArmourNowiki( $nowiki, $contentWithoutTags );
+ $parse = self::sectionise( $contentWithoutTags, $canWrap );
- $tagPlaceHolders[$ph] =
- self::index_replace( $contents, $parse['template'], $start, $end );
+ // Update list of sections and the template with the results
+ $sections += $parse['sections'];
+ $tagPlaceHolders[$ph] = $openTag . $parse['template'] . '</translate>';
}
$prettyTemplate = $text;
@@ -324,15 +321,14 @@ class TranslatablePage {
$prettyTemplate = str_replace( $ph, '[...]', $prettyTemplate );
}
- if ( strpos( $text, '<translate>' ) !== false ) {
+ if ( preg_match( '~<translate( nowrap)?>~', $text ) !== 0 ) {
throw new TPException( [ 'pt-parse-open', $prettyTemplate ] );
} elseif ( strpos( $text, '</translate>' ) !== false ) {
throw new TPException( [ 'pt-parse-close', $prettyTemplate ] );
}
- foreach ( $tagPlaceHolders as $ph => $value ) {
- $text = str_replace( $ph, $value, $text );
- }
+ // Replace the tag placeholders with unit placeholders to form the template
+ $text = strtr( $text, $tagPlaceHolders );
if ( count( $sections ) === 1 ) {
// Don't return display title for pages which have no sections
@@ -362,7 +358,7 @@ class TranslatablePage {
public static function cleanupTags( $text ) {
$nowiki = [];
$text = self::armourNowiki( $nowiki, $text );
- $text = preg_replace( '~<translate>\n?~s', '', $text );
+ $text = preg_replace( '~<translate( nowrap)?>\n?~s', '', $text );
$text = preg_replace( '~\n?</translate>~s', '', $text );
// Mirroring what TPSection::getTextForTrans does
$text = preg_replace( '~<tvar\|([^>]+)>(.*?)</>~u', '\2', $text );
@@ -402,25 +398,15 @@ class TranslatablePage {
}
/**
- * @param string $string
- * @param string $rep
- * @param int $start
- * @param int $end
- * @return string
- */
- protected static function index_replace( $string, $rep, $start, $end ) {
- return substr( $string, 0, $start ) . $rep . substr( $string, $end );
- }
-
- /**
* Splits the content marked with \<translate> tags into sections, which
* are separated with with two or more newlines. Extra whitespace is captured
* in the template and is not included in the sections.
*
* @param string $text Contents of one pair of \<translate> tags.
+ * @param bool $canWrap
* @return array Contains a template and array of unparsed sections.
*/
- public static function sectionise( $text ) {
+ public static function sectionise( string $text, bool $canWrap ): array {
$flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE;
$parts = preg_split( '~(^\s*|\s*\n\n\s*|\s*$)~', $text, -1, $flags );
@@ -436,6 +422,7 @@ class TranslatablePage {
$ph = TranslateUtils::getPlaceholder();
$tpsection = self::shakeSection( $_ );
$tpsection->setIsInline( $inline );
+ $tpsection->setCanWrap( $canWrap );
$sections[$ph] = $tpsection;
$template .= $ph;
}
@@ -507,6 +494,7 @@ class TranslatablePage {
*/
public function addMarkedTag( $revision, $value = null ) {
$this->addTag( 'tp:mark', $revision, $value );
+ self::clearSourcePageCache();
}
/**
@@ -584,6 +572,7 @@ class TranslatablePage {
$dbw->delete( 'revtag', $conds, __METHOD__ );
$dbw->delete( 'translate_sections', [ 'trs_page' => $aid ], __METHOD__ );
unset( self::$tagCache[$aid] );
+ self::clearSourcePageCache();
}
/**
@@ -689,7 +678,7 @@ class TranslatablePage {
* @return string[] List of string
* @since 2012-08-06
*/
- protected function getSections() {
+ public function getSections() {
$dbr = TranslateUtils::getSafeReadDB();
$conds = [ 'trs_page' => $this->getTitle()->getArticleID() ];
@@ -707,7 +696,7 @@ class TranslatablePage {
* Returns a list of translation unit pages.
* @param string $set Can be either 'all', or 'active'
* @param string|bool $code Only list unit pages in given language.
- * @return Title[] List of Titles.
+ * @return Title[]
* @since 2012-08-06
*/
public function getTranslationUnitPages( $set = 'active', $code = false ) {
@@ -851,29 +840,65 @@ class TranslatablePage {
}
/**
+ * Helper to guess translation page from translation unit.
+ *
+ * @param LinkTarget $translationUnit
+ * @return array
+ * @since 2019.10
+ */
+ 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
+ ];
+ }
+
+ /**
* @param Title $title
* @return bool
*/
public static function isSourcePage( Title $title ) {
- $cache = ObjectCache::getMainWANInstance();
- $pcTTL = $cache::TTL_PROC_LONG;
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+ $cacheKey = $cache->makeKey( 'pagetranslation', 'sourcepages' );
$translatablePageIds = $cache->getWithSetCallback(
- $cache->makeKey( 'pagetranslation', 'sourcepages' ),
- $cache::TTL_MINUTE * 5,
+ $cacheKey,
+ $cache::TTL_HOUR * 2,
function ( $oldValue, &$ttl, array &$setOpts ) {
$dbr = wfGetDB( DB_REPLICA );
$setOpts += Database::getCacheSetOptions( $dbr );
return self::getTranslatablePages();
},
- [ 'pcTTL' => $pcTTL, 'pcGroup' => __CLASS__ . ':30' ]
+ [
+ 'checkKeys' => [ $cacheKey ],
+ 'pcTTL' => $cache::TTL_PROC_SHORT,
+ 'pcGroup' => __CLASS__ . ':1'
+ ]
);
return in_array( $title->getArticleID(), $translatablePageIds );
}
/**
+ * Clears the source page cache
+ */
+ public static function clearSourcePageCache(): void {
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+ $cache->touchCheckKey( $cache->makeKey( 'pagetranslation', 'sourcepages' ) );
+ }
+
+ /**
* Get a list of page ids where the latest revision is either tagged or marked
* @return array
*/
diff --git a/MLEB/Translate/tag/TranslatablePageMoveJob.php b/MLEB/Translate/tag/TranslatablePageMoveJob.php
index 47736e82..c0b1e189 100644
--- a/MLEB/Translate/tag/TranslatablePageMoveJob.php
+++ b/MLEB/Translate/tag/TranslatablePageMoveJob.php
@@ -7,6 +7,8 @@
* @license GPL-2.0-or-later
*/
+use MediaWiki\Extensions\Translate\SystemUsers\FuzzyBot;
+
/**
* Contains class with job for moving translation pages. Used together with
* SpecialPageTranslationMovePage class.
@@ -43,7 +45,6 @@ class TranslatablePageMoveJob extends Job {
public function __construct( $title, $params = [] ) {
parent::__construct( __CLASS__, $title, $params );
- $this->params = $params;
}
public function run() {
@@ -117,6 +118,7 @@ class TranslatablePageMoveJob extends Job {
protected function moveMetadata( $oldGroupId, $newGroupId ) {
$types = [ 'prioritylangs', 'priorityforce', 'priorityreason' ];
+ TranslateMetadata::preloadGroups( [ $oldGroupId, $newGroupId ] );
foreach ( $types as $type ) {
$value = TranslateMetadata::get( $oldGroupId, $type );
if ( $value !== false ) {
@@ -126,13 +128,11 @@ class TranslatablePageMoveJob extends Job {
}
// Make the changes in aggregate groups metadata, if present in any of them.
- $groups = MessageGroups::getAllGroups();
- foreach ( $groups as $group ) {
- if ( !$group instanceof AggregateMessageGroup ) {
- continue;
- }
+ $aggregateGroups = MessageGroups::getGroupsByType( AggregateMessageGroup::class );
+ TranslateMetadata::preloadGroups( array_keys( $aggregateGroups ) );
- $subgroups = TranslateMetadata::get( $group->getId(), 'subgroups' );
+ foreach ( $aggregateGroups as $id => $group ) {
+ $subgroups = TranslateMetadata::get( $id, 'subgroups' );
if ( $subgroups === false ) {
continue;
}
diff --git a/MLEB/Translate/tag/TranslateDeleteJob.php b/MLEB/Translate/tag/TranslateDeleteJob.php
index 38d32f89..a25e38d4 100644
--- a/MLEB/Translate/tag/TranslateDeleteJob.php
+++ b/MLEB/Translate/tag/TranslateDeleteJob.php
@@ -8,6 +8,8 @@
* @license GPL-2.0-or-later
*/
+use MediaWiki\Extensions\Translate\SystemUsers\FuzzyBot;
+
/**
* Contains class with job for deleting translatable and translation pages.
*
@@ -22,7 +24,7 @@ class TranslateDeleteJob extends Job {
* @param string $reason
* @return self
*/
- public static function newJob( Title $target, $base, $full, /*User*/$performer, $reason ) {
+ public static function newJob( Title $target, $base, $full, /*User*/ $performer, $reason ) {
$job = new self( $target );
$job->setUser( FuzzyBot::getUser() );
$job->setFull( $full );
@@ -58,8 +60,32 @@ class TranslateDeleteJob extends Job {
$error = '';
$wikipage = new WikiPage( $title );
- $status = $wikipage->doDeleteArticleReal( "{$summary}: $reason", false, 0, true, $error,
- $user, [], 'delete', true );
+ if ( version_compare( MW_VERSION, '1.35', '<' ) ) {
+ $status = $wikipage->doDeleteArticleReal(
+ "{$summary}: $reason",
+ false,
+ 0,
+ true,
+ $error,
+ $user,
+ [],
+ 'delete',
+ true
+ );
+ } else {
+ $status = $wikipage->doDeleteArticleReal(
+ "{$summary}: $reason",
+ $user,
+ false,
+ null,
+ $error,
+ null,
+ [],
+ 'delete',
+ true
+ );
+ }
+
if ( !$status->isGood() ) {
$params = [
'target' => $base,
@@ -93,7 +119,7 @@ class TranslateDeleteJob extends Job {
$entry->publish( $logid );
$tpage = TranslatablePage::newFromTitle( $title );
- $tpage->getTranslationPercentages( true );
+ $tpage->getTranslationPercentages();
foreach ( $tpage->getTranslationPages() as $page ) {
$page->invalidateCache();
}
diff --git a/MLEB/Translate/tag/TranslateMoveJob.php b/MLEB/Translate/tag/TranslateMoveJob.php
deleted file mode 100644
index 8a06258b..00000000
--- a/MLEB/Translate/tag/TranslateMoveJob.php
+++ /dev/null
@@ -1,221 +0,0 @@
-<?php
-/**
- * Contains class with job for moving translation pages.
- *
- * @file
- * @author Niklas Laxström
- * @copyright Copyright © 2008-2010, Niklas Laxström
- * @license GPL-2.0-or-later
- */
-
-/**
- * Contains class with job for moving translation pages. Used together with
- * PageTranslationMovePage class.
- *
- * @ingroup PageTranslation JobQueue
- */
-class TranslateMoveJob extends Job {
- /**
- * @param Title $source
- * @param Title $target
- * @param array $params should include base-source and base-target
- * @param User $performer
- * @return self
- */
- public static function newJob( Title $source, Title $target, array $params,
- /*User*/$performer
- ) {
- $job = new self( $source );
- $job->setUser( FuzzyBot::getUser() );
- $job->setTarget( $target->getPrefixedText() );
- $summary = wfMessage( 'pt-movepage-logreason', $params['base-source'] );
- $summary = $summary->inContentLanguage()->text();
- $job->setSummary( $summary );
- $job->setParams( $params );
- $job->setPerformer( $performer );
- $job->lock();
-
- return $job;
- }
-
- /**
- * @param Title $title
- * @param array $params
- */
- public function __construct( $title, $params = [] ) {
- parent::__construct( __CLASS__, $title, $params );
- }
-
- public function run() {
- // Unfortunately the global is needed until bug is fixed:
- // https://phabricator.wikimedia.org/T51086
- // Once MW >= 1.24 is supported, can use MovePage class.
- global $wgUser;
-
- // Initialization
- $title = $this->title;
- // Other stuff
- $user = $this->getUser();
- $summary = $this->getSummary();
- $target = $this->getTarget();
- $base = $this->params['base-source'];
- $doer = User::newFromName( $this->getPerformer() );
-
- PageTranslationHooks::$allowTargetEdit = true;
- PageTranslationHooks::$jobQueueRunning = true;
- $oldUser = $wgUser;
- $wgUser = $user;
- self::forceRedirects( false );
-
- // Don't check perms, don't leave a redirect
- $ok = $title->moveTo( $target, false, $summary, false );
- if ( !$ok ) {
- $params = [
- 'target' => $target->getPrefixedText(),
- 'error' => $ok,
- ];
-
- $entry = new ManualLogEntry( 'pagetranslation', 'movenok' );
- $entry->setPerformer( $doer );
- $entry->setTarget( $title );
- $entry->setParameters( $params );
- $logid = $entry->insert();
- $entry->publish( $logid );
- }
-
- self::forceRedirects( true );
- PageTranslationHooks::$allowTargetEdit = false;
-
- $this->unlock();
-
- $cache = wfGetCache( CACHE_ANYTHING );
- $key = wfMemcKey( 'translate-pt-move', $base );
-
- $count = $cache->decr( $key );
- $last = (string)$count === '0';
-
- if ( $last ) {
- $cache->delete( $key );
-
- $params = [
- 'target' => $this->params['base-target'],
- ];
-
- $entry = new ManualLogEntry( 'pagetranslation', 'moveok' );
- $entry->setPerformer( $doer );
- $entry->setParameters( $params );
- $entry->setTarget( Title::newFromText( $base ) );
- $logid = $entry->insert();
- $entry->publish( $logid );
-
- PageTranslationHooks::$jobQueueRunning = false;
- }
-
- $wgUser = $oldUser;
-
- return true;
- }
-
- public function setSummary( $summary ) {
- $this->params['summary'] = $summary;
- }
-
- public function getSummary() {
- return $this->params['summary'];
- }
-
- public function setPerformer( $performer ) {
- if ( is_object( $performer ) ) {
- $this->params['performer'] = $performer->getName();
- } else {
- $this->params['performer'] = $performer;
- }
- }
-
- public function getPerformer() {
- return $this->params['performer'];
- }
-
- /**
- * @param Title|string $target
- */
- public function setTarget( $target ) {
- if ( $target instanceof Title ) {
- $this->params['target'] = $target->getPrefixedText();
- } else {
- $this->params['target'] = $target;
- }
- }
-
- public function getTarget() {
- return Title::newFromText( $this->params['target'] );
- }
-
- public function setUser( $user ) {
- if ( is_object( $user ) ) {
- $this->params['user'] = $user->getName();
- } else {
- $this->params['user'] = $user;
- }
- }
-
- /**
- * Get a user object for doing edits.
- * @return User
- */
- public function getUser() {
- return User::newFromName( $this->params['user'], false );
- }
-
- public function setParams( array $params ) {
- foreach ( $params as $k => $v ) {
- $this->params[$k] = $v;
- }
- }
-
- public function lock() {
- $cache = wfGetCache( CACHE_ANYTHING );
- $cache->set( wfMemcKey( 'pt-lock', sha1( $this->title->getPrefixedText() ) ), true );
- $cache->set( wfMemcKey( 'pt-lock', sha1( $this->getTarget()->getPrefixedText() ) ), true );
- }
-
- public function unlock() {
- $cache = wfGetCache( CACHE_ANYTHING );
- $cache->delete( wfMemcKey( 'pt-lock', sha1( $this->title->getPrefixedText() ) ) );
- $cache->delete( wfMemcKey( 'pt-lock', sha1( $this->getTarget()->getPrefixedText() ) ) );
- }
-
- /**
- * Adapted from wfSuppressWarnings to allow not leaving redirects.
- * @param bool $end
- */
- public static function forceRedirects( $end = false ) {
- static $suppressCount = 0;
- static $originalLevel = null;
-
- global $wgGroupPermissions;
- global $wgUser;
-
- if ( $end ) {
- if ( $suppressCount ) {
- --$suppressCount;
- if ( !$suppressCount ) {
- if ( $originalLevel === null ) {
- unset( $wgGroupPermissions['*']['suppressredirect'] );
- } else {
- $wgGroupPermissions['*']['suppressredirect'] = $originalLevel;
- }
- }
- }
- } else {
- if ( !$suppressCount ) {
- $originalLevel = isset( $wgGroupPermissions['*']['suppressredirect'] ) ?
- $wgGroupPermissions['*']['suppressredirect'] :
- null;
- $wgGroupPermissions['*']['suppressredirect'] = true;
- }
- ++$suppressCount;
- }
- $wgUser->clearInstanceCache();
- }
-}
diff --git a/MLEB/Translate/tag/TranslateRenderJob.php b/MLEB/Translate/tag/TranslateRenderJob.php
index a8ccf3c0..27fe1366 100644
--- a/MLEB/Translate/tag/TranslateRenderJob.php
+++ b/MLEB/Translate/tag/TranslateRenderJob.php
@@ -7,12 +7,15 @@
* @license GPL-2.0-or-later
*/
+use MediaWiki\Extensions\Translate\Jobs\GenericTranslateJob;
+use MediaWiki\Extensions\Translate\SystemUsers\FuzzyBot;
+
/**
* Job for updating translation pages when translation or template changes.
*
* @ingroup PageTranslation JobQueue
*/
-class TranslateRenderJob extends Job {
+class TranslateRenderJob extends GenericTranslateJob {
/**
* @param Title $target
@@ -33,21 +36,23 @@ class TranslateRenderJob extends Job {
*/
public function __construct( $title, $params = [] ) {
parent::__construct( __CLASS__, $title, $params );
- $this->params = $params;
$this->removeDuplicates = true;
}
public function run() {
global $wgTranslateKeepOutdatedTranslations;
+ $this->logInfo( 'Starting TranslateRenderJob' );
+
// Initialization
$title = $this->title;
- list( , $code ) = TranslateUtils::figureMessage( $title->getPrefixedText() );
+ [ , $code ] = TranslateUtils::figureMessage( $title->getPrefixedText() );
// Return the actual translation page...
$page = TranslatablePage::isTranslationPage( $title );
if ( !$page ) {
- throw new MWException( "Cannot render translation page for {$title->getPrefixedText()}!" );
+ $this->logError( 'Cannot render translation page!' );
+ return false;
}
$group = $page->getMessageGroup();
@@ -68,10 +73,21 @@ class TranslateRenderJob extends Job {
// @todo FuzzyBot hack
PageTranslationHooks::$allowTargetEdit = true;
$content = ContentHandler::makeContent( $text, $page->getTitle() );
- $page->doEditContent( $content, $summary, $flags, false, $user );
+ $editStatus = $page->doEditContent( $content, $summary, $flags, false, $user );
+ if ( !$editStatus->isOK() ) {
+ $this->logError(
+ 'Error while editing content in page.',
+ [
+ 'content' => $content,
+ 'errors' => $editStatus->getErrors()
+ ]
+ );
+ }
+ $this->logInfo( 'Finished page edit operation' );
PageTranslationHooks::$allowTargetEdit = false;
+ $this->logInfo( 'Finished TranslateRenderJob' );
return true;
}
diff --git a/MLEB/Translate/tag/TranslationsUpdateJob.php b/MLEB/Translate/tag/TranslationsUpdateJob.php
index f3020f41..0384b4ed 100644
--- a/MLEB/Translate/tag/TranslationsUpdateJob.php
+++ b/MLEB/Translate/tag/TranslationsUpdateJob.php
@@ -1,4 +1,6 @@
<?php
+use MediaWiki\Extensions\Translate\Jobs\GenericTranslateJob;
+
/**
* Job for updating translation units and translation pages when
* a translatable page is marked for translation.
@@ -9,7 +11,7 @@
*
* @since 2016.03
*/
-class TranslationsUpdateJob extends Job {
+class TranslationsUpdateJob extends GenericTranslateJob {
/**
* @inheritDoc
*/
@@ -39,6 +41,12 @@ class TranslationsUpdateJob extends Job {
}
public function run() {
+ // WARNING: Nothing here must not depend on message index being up to date.
+ // For performance reasons, message index rebuild is run a separate job after
+ // everything else is updated.
+
+ $this->logInfo( 'Starting TranslationsUpdateJob' );
+
$page = TranslatablePage::newFromTitle( $this->title );
$sections = $this->params[ 'sections' ];
foreach ( $sections as $index => $section ) {
@@ -56,25 +64,46 @@ class TranslationsUpdateJob extends Job {
$job->run();
}
+ $this->logInfo(
+ 'Finished running ' . count( $unitJobs ) . ' MessageUpdate jobs for '
+ . count( $sections ) . ' sections'
+ );
+
// Ensure we are using the latest group definitions. This is needed so
// that in long running scripts we do see the page which was just
// marked for translation. Otherwise getMessageGroup in the next line
// returns null. There is no need to regenerate the global cache.
MessageGroups::singleton()->clearProcessCache();
- // Ensure fresh definitions for MessageIndex and stats
+ // Ensure fresh definitions for stats
$page->getMessageGroup()->clearCaches();
- MessageIndex::singleton()->rebuild();
+ $this->logInfo( 'Cleared caches' );
- // Refresh translations statistics
+ // Refresh translations statistics, we want these to be up to date for the
+ // RenderJobs, for displaying up to date statistics on the translation pages.
$id = $page->getMessageGroupId();
MessageGroupStats::forGroup( $id, MessageGroupStats::FLAG_NO_CACHE );
+ $this->logInfo( 'Updated the message group stats' );
+ // Try to avoid stale statistics on the base page
$wikiPage = WikiPage::factory( $page->getTitle() );
$wikiPage->doPurge();
+ $this->logInfo( 'Finished purging' );
+ // These can be run independently and in parallel if possible
$renderJobs = self::getRenderJobs( $page );
JobQueueGroup::singleton()->push( $renderJobs );
+ $this->logInfo( 'Added ' . count( $renderJobs ) . ' RenderJobs to the queue' );
+
+ // Schedule message index update. Thanks to front caching, it is okay if this takes
+ // a while (and on large wikis it does take a while!). Running it as a separate job
+ // also allows de-duplication in case multiple translatable pages are being marked
+ // for translation in a short period of time.
+ $job = MessageIndexRebuildJob::newJob();
+ JobQueueGroup::singleton()->push( $job );
+
+ $this->logInfo( 'Finished TranslationsUpdateJob' );
+
return true;
}