43 public const LATEST_SYNTAX_VERSION =
'2';
44 public const DEFAULT_SYNTAX_VERSION =
'1';
46 private IConnectionProvider $dbProvider;
47 private JobQueueGroup $jobQueueGroup;
48 private LinkRenderer $linkRenderer;
51 private TitleFormatter $titleFormatter;
52 private TitleParser $titleParser;
58 private WikiPageFactory $wikiPageFactory;
61 private FormatterFactory $formatterFactory;
64 public function __construct(
65 IConnectionProvider $dbProvider,
66 JobQueueGroup $jobQueueGroup,
67 LinkRenderer $linkRenderer,
70 TitleFormatter $titleFormatter,
71 TitleParser $titleParser,
77 WikiPageFactory $wikiPageFactory,
80 FormatterFactory $formatterFactory,
83 $this->dbProvider = $dbProvider;
84 $this->jobQueueGroup = $jobQueueGroup;
85 $this->linkRenderer = $linkRenderer;
86 $this->messageIndex = $messageIndex;
87 $this->titleFormatter = $titleFormatter;
88 $this->titleParser = $titleParser;
89 $this->translatablePageParser = $translatablePageParser;
90 $this->translatablePageStore = $translatablePageStore;
91 $this->translatablePageStateStore = $translatablePageStateStore;
92 $this->translationUnitStoreFactory = $translationUnitStoreFactory;
93 $this->wikiPageFactory = $wikiPageFactory;
94 $this->messageGroups = $messageGroups;
95 $this->messageGroupMetadata = $messageGroupMetadata;
96 $this->translatablePageView = $translatablePageView;
97 $this->messageGroupSubscription = $messageGroupSubscription;
98 $this->formatterFactory = $formatterFactory;
99 $this->hookRunner = $hookRunner;
113 MessageLocalizer $localizer,
116 if ( $removeMarkup ) {
118 $content = ContentHandler::makeContent( $page->getStrippedSourcePageText(), $pageTitle );
120 $wikiPage = $this->wikiPageFactory->newFromTitle( $pageTitle );
121 $updater = $wikiPage->newPageUpdater( $user )
122 ->setContent( SlotRecord::MAIN, $content );
123 $summary = CommentStoreComment::newUnsavedComment(
124 Message::newFromKey(
'tpt-unlink-summary' )->inContentLanguage()->text()
127 if ( $user->authorizeWrite(
'autopatrol', $pageTitle ) ) {
128 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
130 $updater->saveRevision( $summary, EDIT_FORCE_BOT | EDIT_UPDATE );
131 $this->throwIfEditFailed( $updater, $localizer );
134 $this->translatablePageStore->unmark( $page->getPageIdentity() );
136 $entry =
new ManualLogEntry(
'pagetranslation',
'unmark' );
137 $entry->setPerformer( $user );
138 $entry->setTarget( $page->getPageIdentity() );
139 $logId = $entry->insert();
140 $entry->publish( $logId );
157 ?
bool $validateUnitTitle
159 $latestRevID = $page->getLatest();
160 if ( $revision ===
null ) {
162 $revision = $latestRevID;
166 if ( $revision !== $latestRevID ) {
168 $link = $this->linkRenderer->makeKnownLink(
172 [
'oldid' => (
string)$revision ]
176 $this->titleFormatter->getPrefixedText( $page ),
177 Message::rawParam( $link )
184 if ( $translatablePage->getReadyTag() !== $latestRevID ) {
185 throw new TranslatablePageMarkException( [
187 $this->titleFormatter->getPrefixedText( $page ),
188 Message::plaintextParam(
'<translate>' )
195 $isFirstMark = $translatablePage->getMarkedTag() ===
null;
196 if ( $validateUnitTitle ===
null ) {
197 $isTemplateNamespace = $translatablePage->getTitle()->inNamespace( NS_TEMPLATE );
198 $validateUnitTitle = ( $isFirstMark && !$isTemplateNamespace ) || $translatablePage->hasPageDisplayTitle();
201 $parserOutput = $this->translatablePageParser->parse( $translatablePage->getText() );
202 [ $units, $deletedUnits ] = $this->prepareTranslationUnits( $translatablePage, $parserOutput );
205 $defaultState = $validateUnitTitle ?
206 TranslateTitleEnum::DEFAULT_CHECKED :
207 TranslateTitleEnum::DEFAULT_UNCHECKED;
208 $this->hookRunner->onTranslateTitlePageTranslation( $defaultState, $translatablePage->getPageIdentity() );
210 $unitValidationStatus = $this->validateUnitNames(
213 $defaultState !== TranslateTitleEnum::DISABLED && $validateUnitTitle
216 return new TranslatablePageMarkOperation(
222 $unitValidationStatus,
237 private function validateUnitNames(
238 TranslatablePage $page,
240 bool $includePageDisplayTitle
243 $status = Status::newGood();
244 $ic = preg_quote( TranslationUnit::UNIT_MARKER_INVALID_CHARS,
'~' );
245 foreach ( $units as $key => $s ) {
246 $unitStatus = Status::newGood();
247 if ( $includePageDisplayTitle || $key !== TranslatablePage::DISPLAY_TITLE_UNIT_ID ) {
250 $pageTitle = $this->titleFormatter->getPrefixedText( $page->getPageIdentity() );
251 $longestUnitTitle =
"Translations:$pageTitle/{$s->id}/xx-yyyyyyyyyy";
253 $this->titleParser->parseTitle( $longestUnitTitle );
254 }
catch ( MalformedTitleException $e ) {
255 if ( $e->getErrorMessage() ===
'title-invalid-too-long' ) {
257 'tpt-unit-title-too-long',
259 Message::numParam( strlen( $longestUnitTitle ) ),
260 $e->getErrorMessageParameters()[0],
264 $unitStatus->fatal(
'tpt-unit-title-invalid', $s->id, $e->getMessageObject() );
269 if ( $unitStatus->isGood() && preg_match(
"~[$ic]~", $s->id ) ) {
270 $unitStatus->fatal(
'tpt-invalid', $s->id );
276 if ( isset( $usedNames[$s->id] ) ) {
280 $unitStatus->fatal(
'tpt-duplicate', $s->id );
282 $usedNames[$s->id] =
true;
284 $status->merge( $unitStatus );
310 MessageLocalizer $localizer,
313 if ( !$operation->isValid() ) {
314 throw new LogicException(
'Trying to mark a page for translation that is not valid' );
317 $page = $operation->getPage();
318 $title = $page->getTitle();
319 $newRevisionId = $this->updateSectionMarkers( $page, $user, $localizer, $operation );
322 $newRevisionId ??= $title->getLatestRevID();
326 $groupId = $page->getMessageGroupId();
327 $maxId = (int)$this->messageGroupMetadata->get( $groupId,
'maxid' );
329 $pageId = $title->getArticleID();
330 $sections = $pageSettings->shouldTranslateTitle()
331 ? $operation->getUnits()
333 $operation->getUnits(),
334 static fn (
TranslationUnit $s ) => $s->id !== TranslatablePage::DISPLAY_TITLE_UNIT_ID
337 foreach ( array_values( $sections ) as $index => $s ) {
338 $maxId = max( $maxId, (
int)$s->id );
341 if ( in_array( $s->id, $pageSettings->getNoFuzzyUnits(),
true ) ) {
347 'trs_page' => $pageId,
349 'trs_text' => $s->getText(),
350 'trs_order' => $index
354 $dbw = $this->dbProvider->getPrimaryDatabase();
355 $dbw->newDeleteQueryBuilder()
356 ->deleteFrom(
'translate_sections' )
357 ->where( [
'trs_page' => $title->getArticleID() ] )
358 ->caller( __METHOD__ )
362 $dbw->newInsertQueryBuilder()
363 ->insertInto(
'translate_sections' )
365 ->caller( __METHOD__ )
369 $this->saveMetadata( $operation, $pageSettings, $maxId, $user );
371 $page->addMarkedTag( $newRevisionId );
373 if ( $this->translatablePageView->isTranslationBannerNamespaceConfigured() ) {
374 $this->translatablePageStateStore->remove( $page->getPageIdentity() );
378 $this->messageGroups->recache();
381 $newKeys = $group->makeGroupKeys( $changed );
384 $this->messageIndex->storeInterim( $group, $newKeys );
386 $job = UpdateTranslatablePageJob::newFromPage( $page, $sections );
387 $this->jobQueueGroup->push( $job );
390 $entry =
new ManualLogEntry(
'pagetranslation',
'mark' );
391 $entry->setPerformer( $user );
392 $entry->setTarget( $title );
393 $entry->setParameters( [
394 'revision' => $newRevisionId,
395 'changed' => count( $changed ),
397 $logId = $entry->insert();
398 $entry->publish( $logId );
401 $title->invalidateCache();
403 $this->sendNotifications( $sections, $group, $groupId, $operation->isFirstMark() );
405 return count( $sections );
408 private function saveMetadata(
409 TranslatablePageMarkOperation $operation,
410 TranslatablePageSettings $pageSettings,
414 $page = $operation->getPage();
415 $groupId = $page->getMessageGroupId();
417 $this->messageGroupMetadata->set( $groupId,
'maxid', (
string)$maxId );
418 if ( $pageSettings->shouldForceLatestSyntaxVersion() || $operation->isFirstMark() ) {
419 $this->messageGroupMetadata->set( $groupId,
'version', self::LATEST_SYNTAX_VERSION );
422 $this->messageGroupMetadata->set(
425 $pageSettings->shouldEnableTransclusion() ?
'1' :
'0'
428 $this->handlePriorityLanguages( $operation->getPage(), $pageSettings, $user );
431 private function handlePriorityLanguages(
432 TranslatablePage $page,
433 TranslatablePageSettings $pageSettings,
436 $languages = $pageSettings->getPriorityLanguages() ?
437 implode(
',', $pageSettings->getPriorityLanguages() ) :
439 $force = $pageSettings->shouldForcePriorityLanguage() ?
'on' :
false;
440 $hasPriorityConfig = $languages || $force;
444 if ( $hasPriorityConfig && $pageSettings->getPriorityLanguageComment() !==
'' ) {
445 $reason = $pageSettings->getPriorityLanguageComment();
450 $groupId = $page->getMessageGroupId();
452 $opLanguages = $this->messageGroupMetadata->get( $groupId,
'prioritylangs' );
453 $opForce = $this->messageGroupMetadata->get( $groupId,
'priorityforce' );
454 $opReason = $this->messageGroupMetadata->get( $groupId,
'priorityreason' );
456 $this->messageGroupMetadata->set( $groupId,
'prioritylangs', $languages );
457 $this->messageGroupMetadata->set( $groupId,
'priorityforce', $force );
458 $this->messageGroupMetadata->set( $groupId,
'priorityreason', $reason );
461 $opLanguages !== $languages ||
464 ( $opForce !== $force && !( $force ===
false && $opForce ===
'off' ) ) ||
467 ( (
string)$opReason !== (
string)$reason )
469 $logComment = $reason ===
false ?
'' : $reason;
471 'languages' => $languages,
476 $entry =
new ManualLogEntry(
'pagetranslation',
'prioritylanguages' );
477 $entry->setPerformer( $user );
478 $entry->setTarget( $page->getTitle() );
479 $entry->setParameters( $params );
480 $entry->setComment( $logComment );
481 $logId = $entry->insert();
482 $entry->publish( $logId );
486 private function prepareTranslationUnits( TranslatablePage $page, ParserOutput $parserOutput ): array {
487 $highest = (int)$this->messageGroupMetadata->get( $page->getMessageGroupId(),
'maxid' );
489 $store = $this->translationUnitStoreFactory->getReader( $page->getPageIdentity() );
490 $storedUnits = $store->getUnits();
493 $displayTitle =
new TranslationUnit(
494 $this->titleFormatter->getPrefixedText( $page->getPageIdentity() ),
495 TranslatablePage::DISPLAY_TITLE_UNIT_ID
498 $units = [ TranslatablePage::DISPLAY_TITLE_UNIT_ID => $displayTitle ] + $parserOutput->units();
501 foreach ( array_keys( $storedUnits ) as $key ) {
502 $highest = max( $highest, (
int)$key );
504 foreach ( $units as $_ ) {
505 $highest = max( $highest, (
int)$_->id );
508 foreach ( $units as $s ) {
511 if ( $s->id === TranslationUnit::NEW_UNIT_ID ) {
513 $s->id = (string)( ++$highest );
515 if ( isset( $storedUnits[$s->id] ) ) {
516 $storedText = $storedUnits[$s->id]->text;
517 if ( $s->text !== $storedText ) {
518 $s->type =
'changed';
519 $s->oldText = $storedText;
526 $deletedUnits = $storedUnits;
527 foreach ( $units as $s ) {
528 unset( $deletedUnits[$s->id] );
531 return [ $units, $deletedUnits ];
534 private function updateSectionMarkers(
535 TranslatablePage $page,
536 Authority $authority,
537 MessageLocalizer $localizer,
538 TranslatablePageMarkOperation $operation
540 $pageUpdater = $this->wikiPageFactory->newFromTitle( $page->getTitle() )->newPageUpdater( $authority );
541 $content = ContentHandler::makeContent(
542 $operation->getParserOutput()->sourcePageTextForSaving(),
545 $comment = CommentStoreComment::newUnsavedComment(
546 Message::newFromKey(
'tpt-mark-summary' )->inContentLanguage()->text()
549 $pageUpdater->setContent( SlotRecord::MAIN, $content );
550 if ( $authority->authorizeWrite(
'autopatrol', $page->getTitle() ) ) {
551 $pageUpdater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
553 $newRevisionRecord = $pageUpdater->saveRevision( $comment, EDIT_FORCE_BOT | EDIT_UPDATE );
555 $this->throwIfEditFailed( $pageUpdater, $localizer );
557 return $newRevisionRecord !==
null ? $newRevisionRecord->getId() :
null;
560 private function throwIfEditFailed( PageUpdater $pageUpdater, MessageLocalizer $messageLocalizer ): void {
561 $status = $pageUpdater->getStatus();
562 if ( !$status->isOK() ) {
563 throw new TranslatablePageMarkException(
566 $this->formatterFactory->getStatusFormatter( $messageLocalizer )->getMessage( $status ),
584 $changedMessages = array_filter( $sections, static function ( $section ) {
585 return $section->type !==
'old';
588 if ( !$changedMessages ) {
592 if ( $isFirstMark ) {
593 $subscribers = $this->messageGroupSubscription->getGroupSubscribers( $groupId );
594 if ( $subscribers instanceof EmptyIterator ) {
603 $code = $group->getSourceLanguage();
604 $pageTitle = $group->getTitle();
606 foreach ( $changedMessages as $message ) {
607 $messageTitle = Title::makeTitle( NS_TRANSLATIONS,
"$pageTitle/$message->id/$code" );
608 $this->messageGroupSubscription->queueMessage(
610 $message->type ===
'new'
611 ? MessageGroupSubscription::STATE_ADDED
612 : MessageGroupSubscription::STATE_UPDATED,
616 $this->messageGroupSubscription->queueNotificationJob();