36 public const LATEST_SYNTAX_VERSION =
'2';
37 public const DEFAULT_SYNTAX_VERSION =
'1';
39 private ILoadBalancer $loadBalancer;
40 private JobQueueGroup $jobQueueGroup;
41 private LinkRenderer $linkRenderer;
44 private TitleFormatter $titleFormatter;
45 private TitleParser $titleParser;
51 private WikiPageFactory $wikiPageFactory;
54 public function __construct(
55 ILoadBalancer $loadBalancer,
56 JobQueueGroup $jobQueueGroup,
57 LinkRenderer $linkRenderer,
60 TitleFormatter $titleFormatter,
61 TitleParser $titleParser,
67 WikiPageFactory $wikiPageFactory,
70 $this->loadBalancer = $loadBalancer;
71 $this->jobQueueGroup = $jobQueueGroup;
72 $this->linkRenderer = $linkRenderer;
73 $this->messageIndex = $messageIndex;
74 $this->titleFormatter = $titleFormatter;
75 $this->titleParser = $titleParser;
76 $this->translatablePageParser = $translatablePageParser;
77 $this->translatablePageStore = $translatablePageStore;
78 $this->translatablePageStateStore = $translatablePageStateStore;
79 $this->translationUnitStoreFactory = $translationUnitStoreFactory;
80 $this->wikiPageFactory = $wikiPageFactory;
81 $this->messageGroups = $messageGroups;
82 $this->messageGroupMetadata = $messageGroupMetadata;
83 $this->translatablePageView = $translatablePageView;
94 if ( $removeMarkup ) {
95 $content = ContentHandler::makeContent(
96 $page->getStrippedSourcePageText(),
100 $status = $this->wikiPageFactory->newFromTitle( $page->getPageIdentity() )->doUserEditContent(
103 Message::newFromKey(
'tpt-unlink-summary' )->inContentLanguage()->text(),
104 EDIT_FORCE_BOT | EDIT_UPDATE
107 if ( !$status->isOK() ) {
112 $this->translatablePageStore->unmark( $page->getPageIdentity() );
114 $entry =
new ManualLogEntry(
'pagetranslation',
'unmark' );
115 $entry->setPerformer( $user );
116 $entry->setTarget( $page->getPageIdentity() );
117 $logId = $entry->insert();
118 $entry->publish( $logId );
134 bool $validateUnitTitle
136 $latestRevID = $page->getLatest();
137 if ( $revision ===
null ) {
139 $revision = $latestRevID;
143 if ( $revision !== $latestRevID ) {
145 $link = $this->linkRenderer->makeKnownLink(
149 [
'oldid' => (
string)$revision ]
153 $this->titleFormatter->getPrefixedText( $page ),
154 Message::rawParam( $link )
161 if ( $translatablePage->getReadyTag() !== $latestRevID ) {
162 throw new TranslatablePageMarkException( [
164 $this->titleFormatter->getPrefixedText( $page ),
165 Message::plaintextParam(
'<translate>' )
169 $parserOutput = $this->translatablePageParser->parse( $translatablePage->getText() );
170 [ $units, $deletedUnits ] = $this->prepareTranslationUnits( $translatablePage, $parserOutput );
172 $unitValidationStatus = $this->validateUnitNames(
178 return new TranslatablePageMarkOperation(
183 $translatablePage->getMarkedTag() ===
null,
184 $unitValidationStatus
198 private function validateUnitNames(
199 TranslatablePage $page,
201 bool $includePageDisplayTitle
204 $status = Status::newGood();
205 $ic = preg_quote( TranslationUnit::UNIT_MARKER_INVALID_CHARS,
'~' );
206 foreach ( $units as $key => $s ) {
207 $unitStatus = Status::newGood();
208 if ( $includePageDisplayTitle || $key !== TranslatablePage::DISPLAY_TITLE_UNIT_ID ) {
211 $pageTitle = $this->titleFormatter->getPrefixedText( $page->getPageIdentity() );
212 $longestUnitTitle =
"Translations:$pageTitle/{$s->id}/xx-yyyyyyyyyy";
214 $this->titleParser->parseTitle( $longestUnitTitle );
215 }
catch ( MalformedTitleException $e ) {
216 if ( $e->getErrorMessage() ===
'title-invalid-too-long' ) {
218 'tpt-unit-title-too-long',
220 Message::numParam( strlen( $longestUnitTitle ) ),
221 $e->getErrorMessageParameters()[ 0 ],
225 $unitStatus->fatal(
'tpt-unit-title-invalid', $s->id, $e->getMessageObject() );
230 if ( $unitStatus->isGood() && preg_match(
"~[$ic]~", $s->id ) ) {
231 $unitStatus->fatal(
'tpt-invalid', $s->id );
237 if ( isset( $usedNames[$s->id] ) ) {
241 $unitStatus->fatal(
'tpt-duplicate', $s->id );
243 $usedNames[$s->id] =
true;
245 $status->merge( $unitStatus );
272 if ( !$operation->isValid() ) {
273 throw new LogicException(
'Trying to mark a page for translation that is not valid' );
276 $page = $operation->getPage();
277 $newRevisionId = $this->updateSectionMarkers( $page, $user, $operation );
280 $newRevisionId ??= $page->getTitle()->getLatestRevID();
284 $groupId = $page->getMessageGroupId();
285 $maxId = (int)$this->messageGroupMetadata->get( $groupId,
'maxid' );
287 $pageId = $page->getTitle()->getArticleID();
288 $sections = $pageSettings->shouldTranslateTitle()
289 ? $operation->getUnits()
291 $operation->getUnits(),
292 static fn (
TranslationUnit $s ) => $s->id !== TranslatablePage::DISPLAY_TITLE_UNIT_ID
295 foreach ( array_values( $sections ) as $index => $s ) {
296 $maxId = max( $maxId, (
int)$s->id );
299 if ( in_array( $s->id, $pageSettings->getNoFuzzyUnits(),
true ) ) {
305 'trs_page' => $pageId,
307 'trs_text' => $s->getText(),
308 'trs_order' => $index
312 $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
314 'translate_sections',
315 [
'trs_page' => $page->getTitle()->getArticleID() ],
318 $dbw->insert(
'translate_sections', $inserts, __METHOD__ );
320 $this->saveMetadata( $operation, $pageSettings, $maxId, $user );
322 $page->addMarkedTag( $newRevisionId );
324 if ( $this->translatablePageView->isTranslationBannerNamespaceConfigured() ) {
325 $this->translatablePageStateStore->remove( $page->getPageIdentity() );
329 $this->messageGroups->recache();
332 $newKeys = $group->makeGroupKeys( $changed );
335 $this->messageIndex->storeInterim( $group, $newKeys );
337 $job = UpdateTranslatablePageJob::newFromPage( $page, $sections );
338 $this->jobQueueGroup->push( $job );
341 $entry =
new ManualLogEntry(
'pagetranslation',
'mark' );
342 $entry->setPerformer( $user );
343 $entry->setTarget( $page->getTitle() );
344 $entry->setParameters( [
345 'revision' => $newRevisionId,
346 'changed' => count( $changed ),
348 $logId = $entry->insert();
349 $entry->publish( $logId );
352 $page->getTitle()->invalidateCache();
354 return count( $sections );
357 private function saveMetadata(
358 TranslatablePageMarkOperation $operation,
359 TranslatablePageSettings $pageSettings,
363 $page = $operation->getPage();
364 $groupId = $page->getMessageGroupId();
366 $this->messageGroupMetadata->set( $groupId,
'maxid', (
string)$maxId );
367 if ( $pageSettings->shouldForceLatestSyntaxVersion() || $operation->isFirstMark() ) {
368 $this->messageGroupMetadata->set( $groupId,
'version', self::LATEST_SYNTAX_VERSION );
371 $this->messageGroupMetadata->set(
374 $pageSettings->shouldEnableTransclusion() ?
'1' :
'0'
377 $this->handlePriorityLanguages( $operation->getPage(), $pageSettings, $user );
380 private function handlePriorityLanguages(
381 TranslatablePage $page,
382 TranslatablePageSettings $pageSettings,
385 $languages = $pageSettings->getPriorityLanguages() ?
386 implode(
',', $pageSettings->getPriorityLanguages() ) :
388 $force = $pageSettings->shouldForcePriorityLanguage() ?
'on' :
false;
389 $hasPriorityConfig = $languages || $force;
393 if ( $hasPriorityConfig && $pageSettings->getPriorityLanguageComment() !==
'' ) {
394 $reason = $pageSettings->getPriorityLanguageComment();
399 $groupId = $page->getMessageGroupId();
401 $opLanguages = $this->messageGroupMetadata->get( $groupId,
'prioritylangs' );
402 $opForce = $this->messageGroupMetadata->get( $groupId,
'priorityforce' );
403 $opReason = $this->messageGroupMetadata->get( $groupId,
'priorityreason' );
405 $this->messageGroupMetadata->set( $groupId,
'prioritylangs', $languages );
406 $this->messageGroupMetadata->set( $groupId,
'priorityforce', $force );
407 $this->messageGroupMetadata->set( $groupId,
'priorityreason', $reason );
410 $opLanguages !== $languages ||
413 ( $opForce !== $force && !( $force ===
false && $opForce ===
'off' ) ) ||
416 ( (
string)$opReason !== (
string)$reason )
418 $logComment = $reason ===
false ?
'' : $reason;
420 'languages' => $languages,
425 $entry =
new ManualLogEntry(
'pagetranslation',
'prioritylanguages' );
426 $entry->setPerformer( $user );
427 $entry->setTarget( $page->getTitle() );
428 $entry->setParameters( $params );
429 $entry->setComment( $logComment );
430 $logId = $entry->insert();
431 $entry->publish( $logId );
435 private function prepareTranslationUnits( TranslatablePage $page, ParserOutput $parserOutput ): array {
436 $highest = (int)$this->messageGroupMetadata->get( $page->getMessageGroupId(),
'maxid' );
438 $store = $this->translationUnitStoreFactory->getReader( $page->getPageIdentity() );
439 $storedUnits = $store->getUnits();
442 $displayTitle =
new TranslationUnit(
443 $this->titleFormatter->getPrefixedText( $page->getPageIdentity() ),
444 TranslatablePage::DISPLAY_TITLE_UNIT_ID
447 $units = [ TranslatablePage::DISPLAY_TITLE_UNIT_ID => $displayTitle ] + $parserOutput->units();
450 foreach ( array_keys( $storedUnits ) as $key ) {
451 $highest = max( $highest, (
int)$key );
453 foreach ( $units as $_ ) {
454 $highest = max( $highest, (
int)$_->id );
457 foreach ( $units as $s ) {
460 if ( $s->id === TranslationUnit::NEW_UNIT_ID ) {
462 $s->id = (string)( ++$highest );
464 if ( isset( $storedUnits[$s->id] ) ) {
465 $storedText = $storedUnits[$s->id]->text;
466 if ( $s->text !== $storedText ) {
467 $s->type =
'changed';
468 $s->oldText = $storedText;
475 $deletedUnits = $storedUnits;
476 foreach ( $units as $s ) {
477 unset( $deletedUnits[$s->id] );
480 return [ $units, $deletedUnits ];
483 private function updateSectionMarkers(
484 TranslatablePage $page,
485 Authority $authority,
486 TranslatablePageMarkOperation $operation
488 $pageUpdater = $this->wikiPageFactory->newFromTitle( $page->getTitle() )->newPageUpdater( $authority );
489 $content = ContentHandler::makeContent(
490 $operation->getParserOutput()->sourcePageTextForSaving(),
493 $comment = CommentStoreComment::newUnsavedComment(
494 Message::newFromKey(
'tpt-mark-summary' )->inContentLanguage()->text()
497 $pageUpdater->setContent( SlotRecord::MAIN, $content );
498 if ( $authority->authorizeWrite(
'autopatrol', $page->getTitle() ) ) {
499 $pageUpdater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
501 $newRevisionRecord = $pageUpdater->saveRevision( $comment, EDIT_FORCE_BOT | EDIT_UPDATE );
503 $status = $pageUpdater->getStatus();
504 if ( !$status->isOK() ) {
505 throw new TranslatablePageMarkException( [
'tpt-edit-failed', $status->getMessage() ] );
508 return $newRevisionRecord !==
null ? $newRevisionRecord->getId() :
null;