29 private ActorMigration $actorMigration;
30 private UserFactory $userFactory;
31 private RevisionStore $revisionStore;
32 private ILoadBalancer $DBLoadBalancer;
33 private WikiPageFactory $wikiPageFactory;
35 public function __construct() {
36 parent::__construct();
37 $this->addDescription(
'Fuzzy bot command line script.' );
40 'Title pattern or username if user option is provided.'
44 '(optional) Really fuzzy, no dry-run'
48 '(optional) Skip some languages (comma separated)',
54 '(optional) Comment for updating',
60 '(optional) Fuzzy the translations made by user given as an argument.',
64 $this->requireExtension(
'Translate' );
67 private function initServices():
void {
68 $mwServices = MediaWikiServices::getInstance();
69 $this->actorMigration = $mwServices->getActorMigration();
70 $this->userFactory = $mwServices->getUserFactory();
71 $this->revisionStore = $mwServices->getRevisionStore();
72 $this->DBLoadBalancer = $mwServices->getDBLoadBalancer();
73 $this->wikiPageFactory = $mwServices->getWikiPageFactory();
76 public function execute():
void {
77 $this->initServices();
80 if ( $this->hasOption(
'skiplanguages' ) ) {
81 $skipLanguages = array_map(
83 explode(
',', $this->getOption(
'skiplanguages' ) )
87 if ( $this->hasOption(
'user' ) ) {
88 $user = $this->userFactory->newFromName( $this->getArg( 0 ) );
89 $pages = $this->getPagesForUser( $user, $skipLanguages );
91 $pages = $this->getPagesForPattern( $this->getArg( 0 ), $skipLanguages );
94 $dryrun = !$this->hasOption(
'really' );
95 $comment = $this->getOption(
'comment' );
96 $this->fuzzyTranslations( $pages, $dryrun, $comment );
99 private function fuzzyTranslations( array $pages,
bool $dryrun, ?
string $comment ):
void {
100 $count = count( $pages );
101 $this->output(
"Found $count pages to update.",
'pagecount' );
103 foreach ( $pages as [ $title, $text ] ) {
104 $this->updateMessage( $title, TRANSLATE_FUZZY . $text, $dryrun, $comment );
113 private function getMessageContentsFromRows( IResultWrapper $rows ): array {
114 $messagesContents = [];
115 $slots = $this->revisionStore->getContentBlobsForBatch( $rows, [ SlotRecord::MAIN ] )->getValue();
116 foreach ( $rows as $row ) {
117 $title = Title::makeTitle( $row->page_namespace, $row->page_title );
118 if ( isset( $slots[$row->rev_id] ) ) {
119 $text = $slots[$row->rev_id][SlotRecord::MAIN]->blob_data;
121 $content = $this->revisionStore
122 ->newRevisionFromRow( $row, IDBAccessObject::READ_NORMAL, $title )
123 ->getContent( SlotRecord::MAIN );
124 $text = Utilities::getTextFromTextContent( $content );
126 $messagesContents[] = [ $title, $text ];
128 return $messagesContents;
132 private function getPagesForPattern(
string $pattern, array $skipLanguages = [] ): array {
133 $dbr = $this->DBLoadBalancer->getMaintenanceConnectionRef( DB_REPLICA );
136 'page_latest=rev_id',
139 $title = Title::newFromText( $pattern );
140 if ( $title->inNamespace( NS_MAIN ) ) {
141 $namespace = $this->getConfig()->get(
'TranslateMessageNamespaces' );
143 $namespace = $title->getNamespace();
146 $conds[
'page_namespace'] = $namespace;
147 $conds[] =
'page_title' . $dbr->buildLike( $title->getDBkey(), $dbr->anyString() );
149 if ( count( $skipLanguages ) ) {
150 $skiplist = $dbr->makeList( $skipLanguages );
151 $conds[] =
"substring_index(page_title, '/', -1) NOT IN ($skiplist)";
154 $rows = $this->revisionStore->newSelectQueryBuilder( $dbr )
157 ->caller( __METHOD__ )
159 return $this->getMessageContentsFromRows( $rows );
162 private function getPagesForUser( User $user, array $skipLanguages = [] ): array {
163 $dbr = $this->DBLoadBalancer->getMaintenanceConnectionRef( DB_REPLICA );
165 $revWhere = $this->actorMigration->getWhere( $dbr,
'rev_user', $user );
167 'page_latest=rev_id',
169 'page_namespace' => $this->getConfig()->get(
'TranslateMessageNamespaces' ),
170 'page_title' . $dbr->buildLike( $dbr->anyString(),
'/', $dbr->anyString() ),
172 if ( count( $skipLanguages ) ) {
173 $skiplist = $dbr->makeList( $skipLanguages );
174 $conds[] =
"substring_index(page_title, '/', -1) NOT IN ($skiplist)";
177 $rows = $this->revisionStore->newSelectQueryBuilder( $dbr )
181 ->caller( __METHOD__ )
184 return $this->getMessageContentsFromRows( $rows );
194 private function updateMessage( Title $title,
string $text,
bool $dryrun, ?
string $comment =
null ) {
195 $this->output(
"Updating {$title->getPrefixedText()}... ", $title );
197 $documentationLanguageCode = $this->getConfig()->get(
'TranslateDocumentationLanguageCode' );
198 $items = explode(
'/', $title->getText(), 2 );
199 if ( isset( $items[1] ) && $items[1] === $documentationLanguageCode ) {
200 $this->output(
'IGNORED!', $title );
206 $this->output(
'DRY RUN!', $title );
211 $wikiPage = $this->wikiPageFactory->newFromTitle( $title );
212 $summary = CommentStoreComment::newUnsavedComment( $comment ??
'Marking as fuzzy' );
213 $content = ContentHandler::makeContent( $text, $title );
214 $updater = $wikiPage->newPageUpdater( FuzzyBot::getUser() );
216 ->setContent( SlotRecord::MAIN, $content )
217 ->saveRevision( $summary, EDIT_FORCE_BOT | EDIT_UPDATE );
218 $status = $updater->getStatus();
220 $this->output( $status->isOK() ?
'OK' :
'FAILED', $title );