Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
CsvTranslationImporter.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use CommentStoreComment;
7use ContentHandler;
8use MediaWiki\Page\WikiPageFactory;
9use MediaWiki\Permissions\Authority;
10use MediaWiki\Revision\SlotRecord;
12use SplFileObject;
13use Status;
14use Title;
16
25 private $wikiPageFactory;
26
27 public function __construct( WikiPageFactory $wikiPageFactory ) {
28 $this->wikiPageFactory = $wikiPageFactory;
29 }
30
32 public function parseFile( string $csvFilePath ): Status {
33 if ( !file_exists( $csvFilePath ) || !is_file( $csvFilePath ) ) {
34 return Status::newFatal(
35 "CSV file path '$csvFilePath' does not exist, is not readable or is not a file"
36 );
37 }
38
39 $indexedLanguageCodes = [];
40 $currentRowCount = -1;
41 $importData = [];
42 $invalidRows = [
43 'emptyTitleRows' => [],
44 'invalidTitleRows' => [],
45 'groupNotFoundRows' => []
46 ];
47
48 $csvFileContent = new SplFileObject( $csvFilePath, 'r' );
49 while ( !$csvFileContent->eof() ) {
50 // Increment the row count at the beginning since we have a bunch of jump statements
51 // at various placaes
52 ++$currentRowCount;
53
54 $csvRow = $csvFileContent->fgetcsv();
55 if ( $this->isCsvRowEmpty( $csvRow ) ) {
56 continue;
57 }
58
59 if ( $currentRowCount === 0 ) {
60 // Validate the header
61 $status = $this->getLanguagesFromHeader( $csvRow );
62 if ( !$status->isGood() ) {
63 return $status;
64 }
66 $indexedLanguageCodes = $status->getValue();
67 continue;
68 }
69
70 $rowData = [ 'translations' => [] ];
71 $messageTitle = isset( $csvRow[0] ) ? trim( $csvRow[0] ) : null;
72 if ( !$messageTitle ) {
73 $invalidRows['emptyTitleRows'][] = $currentRowCount + 1;
74 continue;
75 }
76
77 $handle = $this->getMessageHandleIfValid( $messageTitle );
78 if ( $handle === null ) {
79 $invalidRows['invalidTitleRows'][] = $currentRowCount + 1;
80 continue;
81 }
82
83 // Ensure that the group is present
84 $group = $handle->getGroup();
85 if ( !$group ) {
86 $invalidRows['groupNotFoundRows'][] = $currentRowCount + 1;
87 continue;
88 }
89
90 $sourceLanguage = $group->getSourceLanguage();
91
92 $rowData['messageTitle'] = $messageTitle;
93 foreach ( $indexedLanguageCodes as $languageCode => $index ) {
94 if ( $sourceLanguage === $languageCode ) {
95 continue;
96 }
97
98 $rowData['translations'][$languageCode] = $csvRow[$index] ?? null;
99 }
100 $importData[] = $rowData;
101 }
102
103 $status = new Status();
104 if ( $invalidRows['emptyTitleRows'] ) {
105 $status->fatal(
106 'Empty message titles found on row(s): ' . implode( ',', $invalidRows['emptyTitleRows'] )
107 );
108 }
109
110 if ( $invalidRows['invalidTitleRows'] ) {
111 $status->fatal(
112 'Invalid message title(s) found on row(s): ' . implode( ',', $invalidRows['invalidTitleRows'] )
113 );
114 }
115
116 if ( $invalidRows['groupNotFoundRows'] ) {
117 $status->fatal(
118 'Group not found for message(s) on row(s) ' . implode( ',', $invalidRows['invalidTitleRows'] )
119 );
120 }
121
122 if ( !$status->isGood() ) {
123 return $status;
124 }
125
126 return Status::newGood( $importData );
127 }
128
130 public function importData(
131 array $messagesWithTranslations,
132 Authority $authority,
133 string $comment,
134 ?callable $progressReporter = null
135 ): Status {
136 $commentStoreComment = CommentStoreComment::newUnsavedComment( $comment );
137
138 // Loop over each translation to import
139 $importStatus = new Status();
140 $failedStatuses = [];
141 $currentTranslation = 0;
142 foreach ( $messagesWithTranslations as $messageTranslation ) {
143 $messageTitleText = $messageTranslation['messageTitle'];
144 $messageTitle = Title::newFromText( $messageTitleText );
145 $messageHandle = new MessageHandle( $messageTitle );
146
147 $translationImportStatuses = [];
148
149 // Import each translation for the current message
150 $translations = $messageTranslation['translations'];
151 foreach ( $translations as $languageCode => $translation ) {
152 // Skip empty translations
153 if ( $translation === null || trim( $translation ) === '' ) {
154 continue;
155 }
156
157 $translationTitle = $messageHandle->getTitleForLanguage( $languageCode );
158
159 // Perform the update for the translation page
160 $updater = $this->wikiPageFactory->newFromTitle( $translationTitle )
161 ->newPageUpdater( $authority );
162 $content = ContentHandler::makeContent( $translation, $translationTitle );
163 $updater->setContent( SlotRecord::MAIN, $content );
164 $updater->setFlags( EDIT_FORCE_BOT );
165 $updater->saveRevision( $commentStoreComment );
166
167 $status = $updater->getStatus();
168 $translationImportStatuses[] = $status;
169 if ( !$status->isOK() ) {
170 $failedStatuses[ $translationTitle->getPrefixedText() ] = $status;
171 }
172 }
173
174 ++$currentTranslation;
175 if ( $progressReporter ) {
176 call_user_func(
177 $progressReporter,
178 $messageTitle,
179 $translationImportStatuses,
180 count( $messagesWithTranslations ),
181 $currentTranslation
182 );
183 }
184 }
185
186 if ( $failedStatuses ) {
187 foreach ( $failedStatuses as $failedStatus ) {
188 $importStatus->merge( $failedStatus );
189 }
190
191 $importStatus->setResult( false, $failedStatuses );
192 }
193
194 return $importStatus;
195 }
196
197 private function getLanguagesFromHeader( array $csvHeader ): Status {
198 if ( count( $csvHeader ) < 2 ) {
199 return Status::newFatal(
200 'CSV has < 2 columns. Assuming that there are no languages to import'
201 );
202 }
203
204 $languageCodesInHeader = array_slice( $csvHeader, 2 );
205 if ( $languageCodesInHeader === [] ) {
206 return Status::newFatal( 'No languages found for import' );
207 }
208
209 $invalidLanguageCodes = [];
210 $indexedLanguageCodes = [];
211 // First two columns are message title and definition
212 $originalLanguageIndex = 2;
213 foreach ( $languageCodesInHeader as $languageCode ) {
214 if ( !TranslateUtils::isSupportedLanguageCode( strtolower( $languageCode ) ) ) {
215 $invalidLanguageCodes[] = $languageCode;
216 } else {
217 // Language codes maybe in upper case, convert to lower case for further use.
218 $indexedLanguageCodes[ strtolower( $languageCode ) ] = $originalLanguageIndex;
219 }
220 ++$originalLanguageIndex;
221 }
222
223 if ( $invalidLanguageCodes ) {
224 return Status::newFatal(
225 'Invalid language codes detected in CSV header: ' . implode( ', ', $invalidLanguageCodes )
226 );
227 }
228
229 return Status::newGood( $indexedLanguageCodes );
230 }
231
232 private function getMessageHandleIfValid( string $messageTitle ): ?MessageHandle {
233 $title = Title::newFromText( $messageTitle );
234 if ( $title === null ) {
235 return null;
236 }
237
238 $handle = new MessageHandle( $title );
239 if ( $handle->isValid() ) {
240 return $handle;
241 }
242
243 return null;
244 }
245
246 private function isCsvRowEmpty( array $csvRow ): bool {
247 return count( $csvRow ) === 1 && ( $csvRow[0] === null || trim( $csvRow[0] ) === '' );
248 }
249}
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), MessageIndex::singleton());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), new RevTagStore(), $services->getDBLoadBalancer());}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
importData(array $messagesWithTranslations, Authority $authority, string $comment, ?callable $progressReporter=null)
Import the data returned from the parseFile method.
Class for pointing to messages, like Title class is for titles.
Essentially random collection of helper functions, similar to GlobalFunctions.php.