44 public const LATEST_SYNTAX_VERSION =
'2';
45 public const DEFAULT_SYNTAX_VERSION =
'1';
47 private IConnectionProvider $dbProvider;
48 private JobQueueGroup $jobQueueGroup;
49 private LinkRenderer $linkRenderer;
52 private TitleFormatter $titleFormatter;
53 private TitleParser $titleParser;
59 private WikiPageFactory $wikiPageFactory;
62 private FormatterFactory $formatterFactory;
65 public function __construct(
66 IConnectionProvider $dbProvider,
67 JobQueueGroup $jobQueueGroup,
68 LinkRenderer $linkRenderer,
71 TitleFormatter $titleFormatter,
72 TitleParser $titleParser,
78 WikiPageFactory $wikiPageFactory,
81 FormatterFactory $formatterFactory,
84 $this->dbProvider = $dbProvider;
85 $this->jobQueueGroup = $jobQueueGroup;
86 $this->linkRenderer = $linkRenderer;
87 $this->messageIndex = $messageIndex;
88 $this->titleFormatter = $titleFormatter;
89 $this->titleParser = $titleParser;
90 $this->translatablePageParser = $translatablePageParser;
91 $this->translatablePageStore = $translatablePageStore;
92 $this->translatablePageStateStore = $translatablePageStateStore;
93 $this->translationUnitStoreFactory = $translationUnitStoreFactory;
94 $this->wikiPageFactory = $wikiPageFactory;
95 $this->messageGroups = $messageGroups;
96 $this->messageGroupMetadata = $messageGroupMetadata;
97 $this->translatablePageView = $translatablePageView;
98 $this->messageGroupSubscription = $messageGroupSubscription;
99 $this->formatterFactory = $formatterFactory;
100 $this->hookRunner = $hookRunner;
114 MessageLocalizer $localizer,
117 if ( $removeMarkup ) {
119 $content = ContentHandler::makeContent( $page->getStrippedSourcePageText(), $pageTitle );
121 $wikiPage = $this->wikiPageFactory->newFromTitle( $pageTitle );
122 $updater = $wikiPage->newPageUpdater( $user )
123 ->setContent( SlotRecord::MAIN, $content );
124 $summary = CommentStoreComment::newUnsavedComment(
125 Message::newFromKey(
'tpt-unlink-summary' )->inContentLanguage()->text()
128 if ( $user->authorizeWrite(
'autopatrol', $pageTitle ) ) {
129 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
131 $updater->saveRevision( $summary, EDIT_FORCE_BOT | EDIT_UPDATE );
132 $this->throwIfEditFailed( $updater, $localizer );
135 $this->translatablePageStore->unmark( $page->getPageIdentity() );
137 $entry =
new ManualLogEntry(
'pagetranslation',
'unmark' );
138 $entry->setPerformer( $user );
139 $entry->setTarget( $page->getPageIdentity() );
140 $logId = $entry->insert();
141 $entry->publish( $logId );
161 ?
bool $translateTitle
163 $latestRevID = $page->getLatest();
164 if ( $revision ===
null ) {
166 $revision = $latestRevID;
170 if ( $revision !== $latestRevID ) {
172 $link = $this->linkRenderer->makeKnownLink(
176 [
'oldid' => (
string)$revision ]
180 $this->titleFormatter->getPrefixedText( $page ),
181 Message::rawParam( $link )
188 if ( $translatablePage->getReadyTag() !== $latestRevID ) {
189 throw new TranslatablePageMarkException( [
191 $this->titleFormatter->getPrefixedText( $page ),
192 Message::plaintextParam(
'<translate>' )
199 $isFirstMark = $translatablePage->getMarkedTag() ===
null;
200 if ( $translateTitle ===
null ) {
201 $isTemplateNamespace = $translatablePage->getTitle()->inNamespace( NS_TEMPLATE );
202 $translateTitle = ( $isFirstMark && !$isTemplateNamespace ) || $translatablePage->hasPageDisplayTitle();
205 $parserOutput = $this->translatablePageParser->parse( $translatablePage->getText() );
206 [ $units, $deletedUnits ] = $this->prepareTranslationUnits( $translatablePage, $parserOutput );
210 $defaultState = $translateTitle ?
211 TranslateTitleEnum::DEFAULT_CHECKED :
212 TranslateTitleEnum::DEFAULT_UNCHECKED;
213 $this->hookRunner->onTranslateTitlePageTranslation(
215 $translatablePage->getPageIdentity(),
219 $unitValidationStatus = $this->validateUnitNames(
222 $defaultState !== TranslateTitleEnum::DISABLED && $translateTitle
225 return new TranslatablePageMarkOperation(
231 $unitValidationStatus,
247 private function validateUnitNames(
248 TranslatablePage $page,
250 bool $includePageDisplayTitle
253 $status = Status::newGood();
254 $ic = preg_quote( TranslationUnit::UNIT_MARKER_INVALID_CHARS,
'~' );
255 foreach ( $units as $key => $s ) {
256 $unitStatus = Status::newGood();
257 if ( $includePageDisplayTitle || $key !== TranslatablePage::DISPLAY_TITLE_UNIT_ID ) {
260 $pageTitle = $this->titleFormatter->getPrefixedText( $page->getPageIdentity() );
261 $longestUnitTitle =
"Translations:$pageTitle/{$s->id}/xx-yyyyyyyyyy";
263 $this->titleParser->parseTitle( $longestUnitTitle );
264 }
catch ( MalformedTitleException $e ) {
265 if ( $e->getErrorMessage() ===
'title-invalid-too-long' ) {
267 'tpt-unit-title-too-long',
269 Message::numParam( strlen( $longestUnitTitle ) ),
270 $e->getErrorMessageParameters()[0],
274 $unitStatus->fatal(
'tpt-unit-title-invalid', $s->id, $e->getMessageObject() );
279 if ( $unitStatus->isGood() && preg_match(
"~[$ic]~", $s->id ) ) {
280 $unitStatus->fatal(
'tpt-invalid', $s->id );
286 if ( isset( $usedNames[$s->id] ) ) {
290 $unitStatus->fatal(
'tpt-duplicate', $s->id );
292 $usedNames[$s->id] =
true;
294 $status->merge( $unitStatus );
322 MessageLocalizer $localizer,
325 if ( !$operation->isValid() ) {
326 throw new LogicException(
'Trying to mark a page for translation that is not valid' );
329 $page = $operation->getPage();
330 $title = $page->getTitle();
331 $newRevisionId = $this->updateSectionMarkers( $page, $user, $localizer, $operation );
334 $newRevisionId ??= $title->getLatestRevID();
338 $groupId = $page->getMessageGroupId();
339 $maxId = (int)$this->messageGroupMetadata->get( $groupId,
'maxid' );
341 $pageId = $title->getId();
342 if ( $pageSettings->shouldTranslateTitle() &&
343 $operation->titleTranslationState === TranslateTitleEnum::DISABLED
345 if ( $operation->titleTranslationStateReason ) {
348 throw new TranslatablePageMarkException(
'tpt-translate-title-disabled' );
351 $sections = $pageSettings->shouldTranslateTitle()
352 ? $operation->getUnits()
354 $operation->getUnits(),
355 static fn ( TranslationUnit $s ) => $s->id !== TranslatablePage::DISPLAY_TITLE_UNIT_ID
358 foreach ( array_values( $sections ) as $index => $s ) {
359 $maxId = max( $maxId, (
int)$s->id );
362 if ( in_array( $s->id, $pageSettings->getNoFuzzyUnits(),
true ) ) {
368 'trs_page' => $pageId,
370 'trs_text' => $s->getText(),
371 'trs_order' => $index
375 $dbw = $this->dbProvider->getPrimaryDatabase();
376 $dbw->newDeleteQueryBuilder()
377 ->deleteFrom(
'translate_sections' )
378 ->where( [
'trs_page' => $pageId ] )
379 ->caller( __METHOD__ )
383 $dbw->newInsertQueryBuilder()
384 ->insertInto(
'translate_sections' )
386 ->caller( __METHOD__ )
390 $this->saveMetadata( $operation, $pageSettings, $maxId, $user );
392 $page->addMarkedTag( $newRevisionId );
394 if ( $this->translatablePageView->isTranslationBannerNamespaceConfigured() ) {
395 $this->translatablePageStateStore->remove( $page->getPageIdentity() );
399 $this->messageGroups->recache();
402 $newKeys = $group->makeGroupKeys( $changed );
405 $this->messageIndex->storeInterim( $group, $newKeys );
407 $job = UpdateTranslatablePageJob::newFromPage( $page, $sections );
408 $this->jobQueueGroup->push( $job );
411 $entry =
new ManualLogEntry(
'pagetranslation',
'mark' );
412 $entry->setPerformer( $user );
413 $entry->setTarget( $title );
414 $entry->setParameters( [
415 'revision' => $newRevisionId,
416 'changed' => count( $changed ),
418 $logId = $entry->insert();
419 $entry->publish( $logId );
422 $title->invalidateCache();
424 $this->sendNotifications( $sections, $group, $groupId, $operation->isFirstMark() );
426 return count( $sections );
429 private function saveMetadata(
430 TranslatablePageMarkOperation $operation,
431 TranslatablePageSettings $pageSettings,
435 $page = $operation->getPage();
436 $groupId = $page->getMessageGroupId();
438 $this->messageGroupMetadata->set( $groupId,
'maxid', (
string)$maxId );
439 if ( $pageSettings->shouldForceLatestSyntaxVersion() || $operation->isFirstMark() ) {
440 $this->messageGroupMetadata->set( $groupId,
'version', self::LATEST_SYNTAX_VERSION );
443 $this->messageGroupMetadata->set(
446 $pageSettings->shouldEnableTransclusion() ?
'1' :
'0'
449 $this->handlePriorityLanguages( $operation->getPage(), $pageSettings, $user );
452 private function handlePriorityLanguages(
453 TranslatablePage $page,
454 TranslatablePageSettings $pageSettings,
457 $languages = $pageSettings->getPriorityLanguages() ?
458 implode(
',', $pageSettings->getPriorityLanguages() ) :
460 $force = $pageSettings->shouldForcePriorityLanguage() ?
'on' :
false;
461 $hasPriorityConfig = $languages || $force;
465 if ( $hasPriorityConfig && $pageSettings->getPriorityLanguageComment() !==
'' ) {
466 $reason = $pageSettings->getPriorityLanguageComment();
471 $groupId = $page->getMessageGroupId();
473 $opLanguages = $this->messageGroupMetadata->get( $groupId,
'prioritylangs' );
474 $opForce = $this->messageGroupMetadata->get( $groupId,
'priorityforce' );
475 $opReason = $this->messageGroupMetadata->get( $groupId,
'priorityreason' );
477 $this->messageGroupMetadata->set( $groupId,
'prioritylangs', $languages );
478 $this->messageGroupMetadata->set( $groupId,
'priorityforce', $force );
479 $this->messageGroupMetadata->set( $groupId,
'priorityreason', $reason );
482 $opLanguages !== $languages ||
485 ( $opForce !== $force && !( $force ===
false && $opForce ===
'off' ) ) ||
488 ( (
string)$opReason !== (
string)$reason )
490 $logComment = $reason ===
false ?
'' : $reason;
492 'languages' => $languages,
497 $entry =
new ManualLogEntry(
'pagetranslation',
'prioritylanguages' );
498 $entry->setPerformer( $user );
499 $entry->setTarget( $page->getTitle() );
500 $entry->setParameters( $params );
501 $entry->setComment( $logComment );
502 $logId = $entry->insert();
503 $entry->publish( $logId );
507 private function prepareTranslationUnits( TranslatablePage $page, ParserOutput $parserOutput ): array {
508 $highest = (int)$this->messageGroupMetadata->get( $page->getMessageGroupId(),
'maxid' );
510 $store = $this->translationUnitStoreFactory->getReader( $page->getPageIdentity() );
511 $storedUnits = $store->getUnits();
514 $displayTitle =
new TranslationUnit(
515 $this->titleFormatter->getPrefixedText( $page->getPageIdentity() ),
516 TranslatablePage::DISPLAY_TITLE_UNIT_ID
519 $units = [ TranslatablePage::DISPLAY_TITLE_UNIT_ID => $displayTitle ] + $parserOutput->units();
522 foreach ( array_keys( $storedUnits ) as $key ) {
523 $highest = max( $highest, (
int)$key );
525 foreach ( $units as $_ ) {
526 $highest = max( $highest, (
int)$_->id );
529 foreach ( $units as $s ) {
532 if ( $s->id === TranslationUnit::NEW_UNIT_ID ) {
534 $s->id = (string)( ++$highest );
536 if ( isset( $storedUnits[$s->id] ) ) {
537 $storedText = $storedUnits[$s->id]->text;
538 if ( $s->text !== $storedText ) {
539 $s->type =
'changed';
540 $s->oldText = $storedText;
549 $deletedUnits = $storedUnits;
550 foreach ( $units as $s ) {
551 unset( $deletedUnits[$s->id] );
554 return [ $units, $deletedUnits ];
557 private function updateSectionMarkers(
558 TranslatablePage $page,
559 Authority $authority,
560 MessageLocalizer $localizer,
561 TranslatablePageMarkOperation $operation
563 $pageUpdater = $this->wikiPageFactory->newFromTitle( $page->getPageIdentity() )->newPageUpdater( $authority );
564 $content = ContentHandler::makeContent(
565 $operation->getParserOutput()->sourcePageTextForSaving(),
568 $comment = CommentStoreComment::newUnsavedComment(
569 Message::newFromKey(
'tpt-mark-summary' )->inContentLanguage()->text()
572 $pageUpdater->setContent( SlotRecord::MAIN, $content );
573 if ( $authority->authorizeWrite(
'autopatrol', $page->getTitle() ) ) {
574 $pageUpdater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
576 $newRevisionRecord = $pageUpdater->saveRevision( $comment, EDIT_FORCE_BOT | EDIT_UPDATE );
578 $this->throwIfEditFailed( $pageUpdater, $localizer );
580 return $newRevisionRecord !==
null ? $newRevisionRecord->getId() :
null;
584 private function throwIfEditFailed( PageUpdater $pageUpdater, MessageLocalizer $messageLocalizer ): void {
585 $status = $pageUpdater->getStatus();
586 if ( !$status->isOK() ) {
587 throw new TranslatablePageMarkException(
590 $this->formatterFactory->getStatusFormatter( $messageLocalizer )->getMessage( $status ),
608 $changedMessages = array_filter( $sections, static function ( $section ) {
609 return $section->type !==
'old';
612 if ( !$changedMessages ) {
616 if ( $isFirstMark ) {
617 $subscribers = $this->messageGroupSubscription->getGroupSubscribers( $groupId );
618 if ( $subscribers instanceof EmptyIterator ) {
627 $code = $group->getSourceLanguage();
628 $pageTitle = $group->getTitle();
630 foreach ( $changedMessages as $message ) {
631 $messageTitle = Title::makeTitle( NS_TRANSLATIONS,
"$pageTitle/$message->id/$code" );
632 $this->messageGroupSubscription->queueMessage(
634 $message->type ===
'new'
635 ? MessageGroupSubscription::STATE_ADDED
636 : MessageGroupSubscription::STATE_UPDATED,
640 $this->messageGroupSubscription->queueNotificationJob();