Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
Utilities.php
1<?php
2declare( strict_types = 1 );
3
5
6use ConfigException;
7use Content;
8use LanguageCode;
12use MediaWiki\Languages\LanguageNameUtils;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Revision\RevisionRecord;
15use MediaWiki\Revision\SlotRecord;
16use MessageGroup;
18use RequestContext;
19use TextContent;
20use Title;
21use UnexpectedValueException;
22use Wikimedia\Rdbms\IDatabase;
23use Xml;
24use XmlSelect;
25
31class Utilities {
40 public static function title( string $message, string $code, int $ns = NS_MEDIAWIKI ): string {
41 // Cache some amount of titles for speed.
42 static $cache = [];
43 $key = $ns . ':' . $message;
44
45 if ( !isset( $cache[$key] ) ) {
46 $cache[$key] = Title::capitalize( $message, $ns );
47 }
48
49 if ( $code ) {
50 return $cache[$key] . '/' . $code;
51 } else {
52 return $cache[$key];
53 }
54 }
55
62 public static function figureMessage( string $text ): array {
63 $pos = strrpos( $text, '/' );
64 $code = substr( $text, $pos + 1 );
65 $key = substr( $text, 0, $pos );
66
67 return [ $key, $code ];
68 }
69
77 public static function getMessageContent( string $key, string $language, int $namespace = NS_MEDIAWIKI ): ?string {
78 $title = self::title( $key, $language, $namespace );
79 $data = self::getContents( [ $title ], $namespace );
80
81 return $data[$title][0] ?? null;
82 }
83
92 public static function getContents( $titles, int $namespace ): array {
93 $mwServices = MediaWikiServices::getInstance();
94 $dbr = $mwServices->getDBLoadBalancer()->getConnection( DB_REPLICA );
95 $revStore = $mwServices->getRevisionStore();
96 $titleContents = [];
97
98 $query = $revStore->getQueryInfo( [ 'page', 'user' ] );
99 $rows = $dbr->select(
100 $query['tables'],
101 $query['fields'],
102 [
103 'page_namespace' => $namespace,
104 'page_title' => $titles,
105 'page_latest=rev_id',
106 ],
107 __METHOD__,
108 [],
109 $query['joins']
110 );
111
112 $revisions = $revStore->newRevisionsFromBatch( $rows, [
113 'slots' => true,
114 'content' => true
115 ] )->getValue();
116
117 foreach ( $rows as $row ) {
119 $rev = $revisions[$row->rev_id];
120 if ( $rev ) {
122 $content = $rev->getContent( SlotRecord::MAIN );
123 if ( $content ) {
124 $titleContents[$row->page_title] = [
125 $content->getText(),
126 $row->rev_user_text
127 ];
128 }
129 }
130 }
131
132 $rows->free();
133
134 return $titleContents;
135 }
136
143 public static function getContentForTitle( Title $title, bool $addFuzzy = false ): ?string {
144 $store = MediaWikiServices::getInstance()->getRevisionStore();
145 $revision = $store->getRevisionByTitle( $title );
146
147 if ( $revision === null ) {
148 return null;
149 }
150
151 $content = $revision->getContent( SlotRecord::MAIN );
152 $wiki = ( $content instanceof TextContent ) ? $content->getText() : null;
153
154 // Either unexpected content type, or the revision content is hidden
155 if ( $wiki === null ) {
156 return null;
157 }
158
159 if ( $addFuzzy ) {
160 $handle = new MessageHandle( $title );
161 if ( $handle->isFuzzy() ) {
162 $wiki = TRANSLATE_FUZZY . str_replace( TRANSLATE_FUZZY, '', $wiki );
163 }
164 }
165
166 return $wiki;
167 }
168
169 /* Some other helpers for output */
170
177 public static function getLanguageName( string $code, ?string $language = 'en' ): string {
178 $languages = self::getLanguageNames( $language );
179 return $languages[$code] ?? $code;
180 }
181
182 // TODO remove languageSelector() after Sunsetting of TranslateSvg extension
183
190 public static function languageSelector( $language, $selectedId ) {
191 $selector = self::getLanguageSelector( $language );
192 $selector->setDefault( $selectedId );
193 $selector->setAttribute( 'id', 'language' );
194 $selector->setAttribute( 'name', 'language' );
195
196 return $selector->getHTML();
197 }
198
205 public static function getLanguageSelector( $language, ?string $labelOption = null ) {
206 $languages = self::getLanguageNames( $language );
207 ksort( $languages );
208
209 $selector = new XmlSelect();
210 if ( $labelOption !== null ) {
211 $selector->addOption( $labelOption, '-' );
212 }
213
214 foreach ( $languages as $code => $name ) {
215 $selector->addOption( "$code - $name", $code );
216 }
217
218 return $selector;
219 }
220
228 public static function getLanguageNames( ?string $code ): array {
229 $mwServices = MediaWikiServices::getInstance();
230 $languageNames = $mwServices->getLanguageNameUtils()->getLanguageNames( $code );
231
232 $deprecatedCodes = LanguageCode::getDeprecatedCodeMapping();
233 foreach ( array_keys( $deprecatedCodes ) as $deprecatedCode ) {
234 unset( $languageNames[ $deprecatedCode ] );
235 }
236 Services::getInstance()->getHookRunner()->onTranslateSupportedLanguages( $languageNames, $code );
237
238 return $languageNames;
239 }
240
247 public static function messageKeyToGroup( int $namespace, string $key ): ?string {
248 $groups = self::messageKeyToGroups( $namespace, $key );
249
250 return count( $groups ) ? $groups[0] : null;
251 }
252
257 public static function messageKeyToGroups( int $namespace, string $key ): array {
258 $mi = Services::getInstance()->getMessageIndex()->retrieve();
259 $normkey = self::normaliseKey( $namespace, $key );
260
261 if ( isset( $mi[$normkey] ) ) {
262 return (array)$mi[$normkey];
263 } else {
264 return [];
265 }
266 }
267
269 public static function normaliseKey( int $namespace, string $key ): string {
270 $key = lcfirst( $key );
271
272 return strtr( "$namespace:$key", ' ', '_' );
273 }
274
282 public static function fieldset( string $legend, string $contents, array $attributes = [] ): string {
283 return Xml::openElement( 'fieldset', $attributes ) .
284 Xml::tags( 'legend', null, $legend ) . $contents .
285 Xml::closeElement( 'fieldset' );
286 }
287
299 public static function convertWhiteSpaceToHTML( string $message ): string {
300 $msg = htmlspecialchars( $message );
301 $msg = preg_replace( '/^ /m', '&#160;', $msg );
302 $msg = preg_replace( '/ $/m', '&#160;', $msg );
303 $msg = preg_replace( '/ /', '&#160; ', $msg );
304 $msg = str_replace( "\n", '<br />', $msg );
305
306 return $msg;
307 }
308
314 public static function cacheFile( string $filename ): string {
315 global $wgTranslateCacheDirectory, $wgCacheDirectory;
316
317 if ( $wgTranslateCacheDirectory !== false ) {
318 $dir = $wgTranslateCacheDirectory;
319 } elseif ( $wgCacheDirectory !== false ) {
320 $dir = $wgCacheDirectory;
321 } else {
322 throw new ConfigException( "\$wgCacheDirectory must be configured" );
323 }
324
325 return "$dir/$filename";
326 }
327
329 public static function getPlaceholder(): string {
330 static $i = 0;
331
332 return "\x7fUNIQ" . dechex( mt_rand( 0, 0x7fffffff ) ) .
333 dechex( mt_rand( 0, 0x7fffffff ) ) . '-' . $i++;
334 }
335
342 public static function getIcon( MessageGroup $g, int $size ): ?array {
343 $icon = $g->getIcon();
344 if ( !$icon || substr( $icon, 0, 7 ) !== 'wiki://' ) {
345 return null;
346 }
347
348 $formats = [];
349
350 $filename = substr( $icon, 7 );
351 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $filename );
352 if ( !$file ) {
353 wfWarn( "Unknown message group icon file $icon" );
354
355 return null;
356 }
357
358 if ( $file->isVectorized() ) {
359 $formats['vector'] = $file->getFullUrl();
360 }
361
362 $formats['raster'] = $file->createThumb( $size, $size );
363
364 return $formats;
365 }
366
373 public static function getSafeReadDB(): IDatabase {
374 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
375 $index = self::shouldReadFromPrimary() ? DB_PRIMARY : DB_REPLICA;
376
377 return $lb->getConnection( $index );
378 }
379
381 public static function shouldReadFromPrimary(): bool {
382 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
383 // Parsing APIs need POST for payloads but are read-only, so avoid spamming
384 // the primary then. No good way to check this at the moment...
385 if ( PageTranslationHooks::$renderingContext ) {
386 return false;
387 }
388
389 return PHP_SAPI === 'cli' ||
390 RequestContext::getMain()->getRequest()->wasPosted() ||
391 $lb->hasOrMadeRecentPrimaryChanges();
392 }
393
399 public static function getEditorUrl( MessageHandle $handle ): string {
400 if ( !$handle->isValid() ) {
401 return $handle->getTitle()->getLocalURL( [ 'action' => 'edit' ] );
402 }
403
404 $title = MediaWikiServices::getInstance()
405 ->getSpecialPageFactory()->getPage( 'Translate' )->getPageTitle();
406 return $title->getFullURL( [
407 'showMessage' => $handle->getInternalKey(),
408 'group' => $handle->getGroup()->getId(),
409 'language' => $handle->getCode(),
410 ] );
411 }
412
417 public static function serialize( $value ): string {
418 return serialize( $value );
419 }
420
425 public static function deserialize( string $str, array $opts = [ 'allowed_classes' => false ] ) {
426 return unserialize( $str, $opts );
427 }
428
429 public static function getVersion(): string {
430 // Avoid parsing JSON multiple time per request
431 static $version = null;
432 if ( $version === null ) {
433 $version = json_decode( file_get_contents( __DIR__ . '../../../extension.json' ) )->version;
434 }
435 return $version;
436 }
437
445 public static function allowsSubpages( Title $title ): bool {
446 $mwInstance = MediaWikiServices::getInstance();
447 $namespaceInfo = $mwInstance->getNamespaceInfo();
448 return $namespaceInfo->hasSubpages( $title->getNamespace() );
449 }
450
457 public static function isSupportedLanguageCode( string $code ): bool {
458 $all = self::getLanguageNames( LanguageNameUtils::AUTONYMS );
459 return isset( $all[ $code ] );
460 }
461
462 public static function getTextFromTextContent( ?Content $content ): string {
463 if ( !$content ) {
464 throw new UnexpectedValueException( 'Expected $content to be TextContent, got null instead.' );
465 }
466
467 if ( $content instanceof TextContent ) {
468 return $content->getText();
469 }
470
471 throw new UnexpectedValueException( 'Expected $content to be TextContent, but got ' . get_class( $content ) );
472 }
473
480 public static function getTranslations( MessageHandle $handle ): array {
481 $namespace = $handle->getTitle()->getNamespace();
482 $base = $handle->getKey();
483
484 $dbr = MediaWikiServices::getInstance()
485 ->getDBLoadBalancer()
486 ->getConnection( DB_REPLICA );
487
488 $titles = $dbr->newSelectQueryBuilder()
489 ->select( 'page_title' )
490 ->from( 'page' )
491 ->where( [
492 'page_namespace' => $namespace,
493 'page_title ' . $dbr->buildLike( "$base/", $dbr->anyString() ),
494 ] )
495 ->caller( __METHOD__ )
496 ->orderBy( 'page_title' )
497 ->fetchFieldValues();
498
499 if ( $titles === [] ) {
500 return [];
501 }
502
503 return self::getContents( $titles, $namespace );
504 }
505
506 public static function isTranslationPage( MessageHandle $handle ): bool {
507 // FIXME: A lot of this code is similar to TranslatablePage::isTranslationPage.
508 // See if they can be merged
509 // The major difference is that this method does not run a database query to check if
510 // the page is marked.
511 $key = $handle->getKey();
512 $languageCode = $handle->getCode();
513 if ( $key === '' || $languageCode === '' ) {
514 return false;
515 }
516
517 $baseTitle = Title::makeTitle( $handle->getTitle()->getNamespace(), $key );
518 if ( !TranslatablePage::isSourcePage( $baseTitle ) ) {
519 return false;
520 }
521
522 static $codes = null;
523 if ( $codes === null ) {
524 $codes = self::getLanguageNames( LanguageNameUtils::AUTONYMS );
525 }
526
527 return !$handle->isDoc() && isset( $codes[ $languageCode ] );
528 }
529}
530
531class_alias( Utilities::class, 'TranslateUtils' );
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'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, '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:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, '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:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, '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:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, '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(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(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(), $services->getDBLoadBalancer());}, '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
Mixed bag of methods related to translatable pages.
Minimal service container.
Definition Services.php:44
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31
static getPlaceholder()
Returns a random string that can be used as placeholder in strings.
static messageKeyToGroup(int $namespace, string $key)
Returns the primary group message belongs to.
static allowsSubpages(Title $title)
Checks if the namespace that the title belongs to allows subpages.
static getIcon(MessageGroup $g, int $size)
Get URLs for icons if available.
static getMessageContent(string $key, string $language, int $namespace=NS_MEDIAWIKI)
Loads page content without side effects.
Definition Utilities.php:77
static getLanguageName(string $code, ?string $language='en')
Returns a localised language name.
static getEditorUrl(MessageHandle $handle)
Get a URL that points to an editor for this message handle.
static fieldset(string $legend, string $contents, array $attributes=[])
Constructs a fieldset with contents.
static isSupportedLanguageCode(string $code)
Checks whether a language code is supported for translation at the wiki level.
static languageSelector( $language, $selectedId)
Returns a language selector.
static getTranslations(MessageHandle $handle)
Returns all translations of a given message.
static getContentForTitle(Title $title, bool $addFuzzy=false)
Returns the content for a given title and adds the fuzzy tag if requested.
static getSafeReadDB()
Get a DB handle suitable for read and read-for-write cases.
static getContents( $titles, int $namespace)
Fetches contents for pagenames in given namespace without side effects.
Definition Utilities.php:92
static messageKeyToGroups(int $namespace, string $key)
Returns the all the groups message belongs to.
static shouldReadFromPrimary()
Check whether primary should be used for reads to avoid reading stale data.
static serialize( $value)
Serialize the given value.
static cacheFile(string $filename)
Gets the path for cache files.
static convertWhiteSpaceToHTML(string $message)
Escapes the message, and does some mangling to whitespace, so that it is preserved when outputted as-...
static normaliseKey(int $namespace, string $key)
Converts page name and namespace to message index format.
static deserialize(string $str, array $opts=[ 'allowed_classes'=> false])
Deserialize the given string.
static title(string $message, string $code, int $ns=NS_MEDIAWIKI)
Does quick normalisation of message name so that in can be looked from the database.
Definition Utilities.php:40
static getLanguageSelector( $language, ?string $labelOption=null)
Standard language selector in Translate extension.
static figureMessage(string $text)
Splits page name into message key and language code.
Definition Utilities.php:62
static getLanguageNames(?string $code)
Get translated language names for the languages generally supported for translation in the current wi...
Class for pointing to messages, like Title class is for titles.
isDoc()
Determine whether the current handle is for message documentation.
getTitle()
Get the original title.
getCode()
Returns the language code.
getKey()
Returns the identified or guessed message key.
Interface for message groups.